A Groq-Powered Agentic Research Assistant with LangGraph, Calling Tools, Sub-Agents, and Agentic Memory: Let’s Build

In this lesson, we create ia Groq-a powerful agent research workflow that works directly using Groq’s OpenAI-compatible inference endpoint. We adapt LangChain’s ChatOpenAI interface to work with Groq by setting a Groq API key and base URL, allowing us to use fast managed models like llama-3.3-70b-variables for tool-based reasoning. We then connect the model to practical tools for web searching, web page downloading, file management, Python implementation, load capabilities, sub-agent deployment, and long-term memory. By the end of the course, we have a multi-step agent based on Groq that can research a topic, send focused subtasks, generate structured results, and save useful information for later implementation.
import subprocess, sys
def _pip(*a): subprocess.check_call([sys.executable,"-m","pip","install","-q",*a])
_pip("langgraph>=0.2.50", "langchain>=0.3.0", "langchain-openai>=0.2.0",
"langchain-community>=0.3.0", "ddgs", "requests", "beautifulsoup4",
"tiktoken", "pydantic>=2.0")
import os, getpass
if not os.environ.get("GROQ_API_KEY"):
os.environ["GROQ_API_KEY"] = getpass.getpass("GROQ_API_KEY (free at console.groq.com/keys): ")
os.environ["OPENAI_API_KEY"] = os.environ["GROQ_API_KEY"]
os.environ["OPENAI_BASE_URL"] = "
MODEL_NAME = "llama-3.3-70b-versatile"
import json, re, io, contextlib, pathlib
from typing import Annotated, TypedDict, Sequence, Literal, List, Dict, Any
from datetime import datetime, timezone
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
SystemMessage, HumanMessage, AIMessage, ToolMessage, BaseMessage)
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNodeWe include libraries needed to build Groq agent workflows, including LangGraph, LangChain, DuckDuckGo search utilities, and support analysis libraries. We securely collect the Groq API key and configure Groq as an OpenAI compatible endpoint by setting the API key and base URL. We then import all the necessary modules for messaging, tools, graphing, typing, file system management, and model implementation.
SANDBOX = pathlib.Path("/content/deerflow_sandbox").resolve()
for sub in ["uploads","workspace","outputs","skills/public","skills/custom","memory"]:
(SANDBOX/sub).mkdir(parents=True, exist_ok=True)
def _safe(p: str) -> pathlib.Path:
full = (SANDBOX/p.lstrip("/")).resolve()
if not str(full).startswith(str(SANDBOX)):
raise ValueError(f"path escapes sandbox: {p}")
return full
SKILLS: Dict[str, Dict[str,str]] = {}
def register_skill(name, description, content, location="public"):
d = SANDBOX/"skills"/location/name; d.mkdir(parents=True, exist_ok=True)
(d/"SKILL.md").write_text(content)
SKILLS[name] = {"description": description, "content": content,
"path": str(d/"SKILL.md")}
register_skill("research",
"Conduct multi-source web research on a topic and produce structured notes.",
"""# Research Skill
## Workflow
1. Decompose the question into 3-5 sub-questions.
2. For each sub-question call `web_search` and pick 2 authoritative URLs.
3. `web_fetch` those URLs; extract concrete facts, numbers, dates.
4. Cross-reference for consensus vs. disagreement.
5. Append findings to `workspace/research_notes.md`: claim → evidence → URL.
## Best practices
- Prefer primary sources. Note dates. Never fabricate URLs or numbers.""")
register_skill("report-generation",
"Synthesize research notes into a polished markdown report in outputs/.",
"""# Report Generation Skill
## Workflow
1. file_read('workspace/research_notes.md').
2. Outline: exec summary, key findings, analysis, conclusion, sources.
3. file_write('outputs/report.md', ...).
## Structure
- # Title
- ## Executive Summary (3–5 sentences)
- ## Key Findings (bullets)
- ## Detailed Analysis (sections)
- ## Conclusion
- ## Sources (numbered URL list)""")
register_skill("code-execution",
"Run Python in the sandbox for computation, data wrangling, charts.",
"""# Code Execution Skill
1. Plan in plain language first.
2. python_exec the code; persistent artifacts go to /outputs/.
3. Verify before quoting results.""")
MEM = SANDBOX/"memory/long_term.json"
if not MEM.exists():
MEM.write_text(json.dumps({"facts":[],"preferences":{}}, indent=2))
def _load_mem(): return json.loads(MEM.read_text())
def _save_mem(m): MEM.write_text(json.dumps(m, indent=2))We created a sandboxed project directory in Colab to keep uploads, workspace files, outputs, capabilities, and memory organized in one controlled place. We describe reusable capabilities for research, report generation, and coding so that an agent can discover and follow a structured workflow. We also implement a simple JSON file for long-term memory that stores facts and preferences across multiple runs within the same sandbox.
@tool
def list_skills() -> str:
"""List all skills with one-line descriptions. Call this first for complex tasks."""
return "n".join(f"- {n}: {s['description']}" for n,s in SKILLS.items())
@tool
def load_skill(name: str) -> str:
"""Load full SKILL.md for `name`. Call before running its workflow."""
if name not in SKILLS: return f"Unknown. Available: {list(SKILLS)}"
return SKILLS[name]["content"]
@tool
def web_search(query: str, max_results: int = 5) -> str:
"""Search the web (DuckDuckGo). Returns titles, URLs, snippets."""
from ddgs import DDGS
out = []
try:
with DDGS() as d:
for r in d.text(query, max_results=max_results):
out.append(f"- {r.get('title','')}n URL: {r.get('href','')}n "
f"{(r.get('body') or '')[:220]}")
except Exception as e:
return f"search error: {e}"
return "n".join(out) or "no results"
@tool
def web_fetch(url: str, max_chars: int = 4000) -> str:
"""Fetch a URL, return cleaned text (scripts/nav stripped)."""
import requests
from bs4 import BeautifulSoup
try:
r = requests.get(url, timeout=15,
headers={"User-Agent":"Mozilla/5.0 DeerFlow-Lite"})
soup = BeautifulSoup(r.text, "html.parser")
for s in soup(["script","style","nav","footer","aside","header"]): s.decompose()
text = re.sub(r"ns*n", "nn", soup.get_text("n")).strip()
return text[:max_chars] or "(empty page)"
except Exception as e:
return f"fetch error: {e}"
@tool
def file_write(path: str, content: str) -> str:
"""Write content to a sandbox path, e.g. 'workspace/notes.md' or 'outputs/x.md'."""
p = _safe(path); p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content)
return f"wrote {len(content)} chars → {path}"
@tool
def file_read(path: str) -> str:
"""Read a sandbox file (first 8 KB)."""
p = _safe(path)
return p.read_text()[:8000] if p.exists() else f"not found: {path}"
@tool
def file_list(path: str = "") -> str:
"""List files under a sandbox dir."""
base = _safe(path) if path else SANDBOX
if not base.exists(): return "not found"
items = []
for c in sorted(base.rglob("*")):
if "memory" in c.relative_to(SANDBOX).parts: continue
items.append(f" {'D' if c.is_dir() else 'F'} {c.relative_to(SANDBOX)}")
return "n".join(items[:60]) or "(empty)"
@tool
def python_exec(code: str) -> str:
"""Run Python in the sandbox. SANDBOX_ROOT is preset."""
g = {"__name__":"__sb__", "SANDBOX_ROOT": str(SANDBOX)}
buf = io.StringIO()
try:
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
exec(code, g)
return (buf.getvalue() or "(no stdout)")[:4000]
except Exception as e:
return f"{type(e).__name__}: {e}n{buf.getvalue()[:1500]}"
@tool
def remember(fact: str) -> str:
"""Persist a single fact to long-term memory (survives across runs)."""
m = _load_mem()
m["facts"].append({"fact": fact, "ts": datetime.now(timezone.utc).isoformat()})
_save_mem(m)
return f"remembered ({len(m['facts'])} total)"
@tool
def recall() -> str:
"""Retrieve everything in long-term memory."""
m = _load_mem()
if not m["facts"]: return "(memory empty)"
return "n".join(f"- {f['fact']}" for f in m["facts"][-20:])We describe the main tools a Groq-backed agent can call at runtime, including indexing capabilities, loading capability instructions, searching the web, downloading web pages, reading files, and writing files. We also provide the agent with a sandboxed Python environment to run calculations or generate artifacts when needed. We add memory tools that allow the agent to recall important facts and recall previously stored information.
@tool
def spawn_subagent(role: str, task: str,
allowed_tools: str = "web_search,web_fetch,file_write,file_read") -> str:
"""Spawn an isolated sub-agent with a focused role and scoped tools.
Returns its final report string. Use for parallelizable / focused subtasks."""
bag = {t.name: t for t in BASE_TOOLS}
sub_tools = [bag[n.strip()] for n in allowed_tools.split(",") if n.strip() in bag]
sub_llm = ChatOpenAI(model=MODEL_NAME, temperature=0.2).bind_tools(sub_tools)
sys_msg = SystemMessage(content=(
f"You are a specialized sub-agent. Role: {role}.n"
f"You operate in an ISOLATED context — no access to lead history.n"
f"Tools: {', '.join(t.name for t in sub_tools)}.n"
"End with a final assistant message starting 'FINAL REPORT:' "
"containing a structured ≤700-word summary including any URLs."))
msgs: List[BaseMessage] = [sys_msg, HumanMessage(content=task)]
for _ in range(8):
r = sub_llm.invoke(msgs); msgs.append(r)
if not getattr(r, "tool_calls", None):
return f"[sub-agent: {role}]n" + (r.content if isinstance(r.content,str) else str(r.content))
for tc in r.tool_calls:
t = bag.get(tc["name"])
try:
res = t.invoke(tc["args"]) if t else f"unknown tool {tc['name']}"
except Exception as e:
res = f"tool error: {e}"
msgs.append(ToolMessage(content=str(res)[:3000], tool_call_id=tc["id"]))
return f"[sub-agent: {role}] step-limit reached."
BASE_TOOLS = [list_skills, load_skill, web_search, web_fetch, file_write,
file_read, file_list, python_exec, remember, recall]
ALL_TOOLS = BASE_TOOLS + [spawn_subagent]
LEAD_SYSTEM = f"""You are DeerFlow-Lite, a long-horizon super-agent harness.
Sandbox layout (relative to {SANDBOX}):
uploads/ – user files
workspace/ – your scratchpad
outputs/ – final deliverables
skills/ – capability modules (load_skill)
Principles:
• For non-trivial tasks: list_skills → load_skill → execute.
• Use spawn_subagent for focused subtasks (isolated context keeps lead lean).
• Persist intermediates to workspace/, deliverables to outputs/.
• Use remember(fact) for cross-session knowledge.
• Finish with a short summary of what was produced and where.
Today: {datetime.now(timezone.utc).strftime('%Y-%m-%d')}."""
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
llm = ChatOpenAI(model=MODEL_NAME, temperature=0.3).bind_tools(ALL_TOOLS)
def call_model(state: AgentState):
msgs = list(state["messages"])
if not msgs or not isinstance(msgs[0], SystemMessage):
msgs = [SystemMessage(content=LEAD_SYSTEM)] + msgs
return {"messages": [llm.invoke(msgs)]}
def route(state: AgentState) -> Literal["tools","__end__"]:
last = state["messages"][-1]
return "tools" if getattr(last, "tool_calls", None) else END
g = StateGraph(AgentState)
g.add_node("agent", call_model)
g.add_node("tools", ToolNode(ALL_TOOLS))
g.set_entry_point("agent")
g.add_conditional_edges("agent", route, {"tools":"tools", END: END})
g.add_edge("tools", "agent")
APP = g.compile()We developed a micro-agent tool that allows Groq’s main agent to delegate focused tasks to a single agent with a limited set of tools. We then gather all the available tools, define the lead system information, implement a Groq-based dialog model, and tie the tools to it. Finally we built a LangGraph workflow so that the agent can alternate between thinking and using the tool until it reaches the final answer.
def run(task: str, max_steps: int = 25):
print("="*78); print(f"🦌 TASK: {task}"); print("="*78)
state = {"messages":[HumanMessage(content=task)]}
n = 0
for ev in APP.stream(state, {"recursion_limit": max_steps*2}, stream_mode="updates"):
for node, payload in ev.items():
for m in payload.get("messages", []):
n += 1
if isinstance(m, AIMessage):
if m.tool_calls:
for tc in m.tool_calls:
args = json.dumps(tc["args"], ensure_ascii=False)
args = args[:140] + ("…" if len(args)>140 else "")
print(f"[{n:02}] 🔧 {tc['name']}({args})")
else:
txt = m.content if isinstance(m.content,str) else str(m.content)
print(f"[{n:02}] 🦌 {txt[:800]}")
elif isinstance(m, ToolMessage):
s = str(m.content).replace("n"," ")[:220]
print(f"[{n:02}] 📤 {s}")
print("n"+"="*78); print("✅ COMPLETE — sandbox state:"); print("="*78)
print(file_list.invoke({"path":""}))
print("n🧠 Long-term memory:"); print(recall.invoke({}))
for f in sorted((SANDBOX/"outputs").rglob("*")):
if f.is_file():
print(f"n--- 📄 {f.relative_to(SANDBOX)} (first 800 chars) ---")
print(f.read_text()[:800])
run(
"Give me a briefing on small language models (SLMs) in 2025. "
"(1) discover skills; (2) spawn a researcher sub-agent to gather "
"specifics on three notable SLMs from 2024-2025 with sizes, benchmarks, "
"and use cases — sub-agent saves to workspace/slm_research.md; "
"(3) load report-generation skill and write outputs/slm_briefing.md "
"(~400 words) with a Sources section; (4) save the single most "
"important takeaway to long-term memory; (5) summarize.",
max_steps=25,
)
We define a run() function that starts the user task, broadcasts each step of the agent, and prints the tool calls, tool output, and final responses in a human-readable format. We also show the sandbox file structure, long-term memory, and output files generated after the operation is complete. We finish by performing a demo task in which a Groq-powered agent searches for sub-language models, prepares information, saves a report, and stores a single memorized key.
In conclusion, we have created a compact yet capable Groq-based agent framework that demonstrates how Groq’s OpenAI-compatible API can serve as a fast, accessible backup for advanced LLM workflows. We used LangGraph to control the agent loop, LangChain to bind tools to the Groq managed model, and custom Python utilities to give the controlled program access to search, files, code execution, and memory. We also demonstrated how isolated sub-agents can help manage focused research tasks while a central agent coordinates the overall workflow. Also, we ended up with a Groq-powered agent system that can be expanded into research assistants, automated information generators, and multi-step AI applications.
Check it out Full Codes with Notebook here. Also, feel free to follow us Twitter and don’t forget to join our 130k+ ML SubReddit and Subscribe to Our newspaper. Wait! are you on telegram? now you can join us on telegram too.
Need to work with us on developing your GitHub Repo OR Hug Face Page OR Product Release OR Webinar etc.? Connect with us



