|

A Groq-Powered Agentic Research Assistant with LangGraph, Tool Calling, Sub-Agents, and Agentic Memory: Lets Built It

🦌

In this tutorial, we construct a Groq-powered agentic analysis workflow that runs instantly utilizing Groq’s free OpenAI-compatible inference endpoint. We configure LangChain’s ChatOpenAI interface to work with Groq by setting the Groq API key and base URL, permitting us to make use of quick hosted fashions corresponding to llama-3.3-70b-versatile for tool-based reasoning. We then join the mannequin with sensible instruments for net search, webpage fetching, file dealing with, Python execution, ability loading, sub-agent delegation, and long-term reminiscence. By the top of the tutorial, we now have a working Groq-based multi-step agent that may analysis a subject, delegate centered subtasks, generate structured outputs, and save helpful info for later runs.

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"] = "https://api.groq.com/openai/v1"


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.instruments import device
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode

We set up the core libraries required to construct the Groq-powered agent workflow, together with LangGraph, LangChain, DuckDuckGo search utilities, and supporting parsing libraries. We securely gather the Groq API key and configure Groq as an OpenAI-compatible endpoint by setting the API key and base URL. We then import all required modules for messages, instruments, graph development, typing, filesystem dealing with, and mannequin initialization.

SANDBOX = pathlib.Path("/content material/deerflow_sandbox").resolve()
for sub in ["uploads","workspace","outputs","skills/public","skills/custom","memory"]:
   (SANDBOX/sub).mkdir(dad and mom=True, exist_ok=True)


def _safe(p: str) -> pathlib.Path:
   full = (SANDBOX/p.lstrip("/")).resolve()
   if not str(full).startswith(str(SANDBOX)):
       elevate ValueError(f"path escapes sandbox: {p}")
   return full


SKILLS: Dict[str, Dict[str,str]] = {}
def register_skill(title, description, content material, location="public"):
   d = SANDBOX/"abilities"/location/title; d.mkdir(dad and mom=True, exist_ok=True)
   (d/"SKILL.md").write_text(content material)
   SKILLS[name] = {"description": description, "content material": content material,
                   "path": str(d/"SKILL.md")}


register_skill("analysis",
   "Conduct multi-source net analysis on a subject and produce structured notes.",
   """# Research Skill
## Workflow
1. Decompose the query into 3-5 sub-questions.
2. For every sub-question name `web_search` and choose 2 authoritative URLs.
3. `web_fetch` these URLs; extract concrete information, numbers, dates.
4. Cross-reference for consensus vs. disagreement.
5. Append findings to `workspace/research_notes.md`: declare → proof → URL.
## Best practices
- Prefer main sources. Note dates. Never fabricate URLs or numbers.""")


register_skill("report-generation",
   "Synthesize analysis notes into a sophisticated markdown report in outputs/.",
   """# Report Generation Skill
## Workflow
1. file_read('workspace/research_notes.md').
2. Outline: exec abstract, key findings, evaluation, 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 record)""")


register_skill("code-execution",
   "Run Python within the sandbox for computation, information wrangling, charts.",
   """# Code Execution Skill
1. Plan in plain language first.
2. python_exec the code; persistent artifacts go to /outputs/.
3. Verify earlier than quoting outcomes.""")


MEM = SANDBOX/"reminiscence/long_term.json"
if not MEM.exists():
   MEM.write_text(json.dumps({"information":[],"preferences":{}}, indent=2))
def _load_mem(): return json.masses(MEM.read_text())
def _save_mem(m): MEM.write_text(json.dumps(m, indent=2))

We create a sandboxed challenge listing in Colab to maintain uploads, workspace recordsdata, outputs, abilities, and reminiscence organized in a single managed location. We outline reusable abilities for analysis, report technology, and code execution so the agent can uncover and comply with structured workflows. We additionally initialize a easy long-term reminiscence JSON file that shops information and preferences throughout a number of runs inside the identical sandbox.

@device
def list_skills() -> str:
   """List all abilities with one-line descriptions. Call this primary for complicated duties."""
   return "n".be a part of(f"- {n}: {s['description']}" for n,s in SKILLS.gadgets())


@device
def load_skill(title: str) -> str:
   """Load full SKILL.md for `title`. Call earlier than operating its workflow."""
   if title not in SKILLS: return f"Unknown. Available: {record(SKILLS)}"
   return SKILLS[name]["content"]


@device
def web_search(question: str, max_results: int = 5) -> str:
   """Search the net (DuckDuckGo). Returns titles, URLs, snippets."""
   from ddgs import DDGS
   out = []
   strive:
       with DDGS() as d:
           for r in d.textual content(question, max_results=max_results):
               out.append(f"- {r.get('title','')}n  URL: {r.get('href','')}n  "
                          f"{(r.get('physique') or '')[:220]}")
   besides Exception as e:
       return f"search error: {e}"
   return "n".be a part of(out) or "no outcomes"


@device
def web_fetch(url: str, max_chars: int = 4000) -> str:
   """Fetch a URL, return cleaned textual content (scripts/nav stripped)."""
   import requests
   from bs4 import BeautifulSoup
   strive:
       r = requests.get(url, timeout=15,
                        headers={"User-Agent":"Mozilla/5.0 DeerFlow-Lite"})
       soup = BeautifulSoup(r.textual content, "html.parser")
       for s in soup(["script","style","nav","footer","aside","header"]): s.decompose()
       textual content = re.sub(r"ns*n", "nn", soup.get_text("n")).strip()
       return textual content[:max_chars] or "(empty web page)"
   besides Exception as e:
       return f"fetch error: {e}"


@device
def file_write(path: str, content material: str) -> str:
   """Write content material to a sandbox path, e.g. 'workspace/notes.md' or 'outputs/x.md'."""
   p = _safe(path); p.dad or mum.mkdir(dad and mom=True, exist_ok=True)
   p.write_text(content material)
   return f"wrote {len(content material)} chars → {path}"


@device
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 discovered: {path}"


@device
def file_list(path: str = "") -> str:
   """List recordsdata underneath a sandbox dir."""
   base = _safe(path) if path else SANDBOX
   if not base.exists(): return "not discovered"
   gadgets = []
   for c in sorted(base.rglob("*")):
       if "reminiscence" in c.relative_to(SANDBOX).components: proceed
       gadgets.append(f"  {'D' if c.is_dir() else 'F'}  {c.relative_to(SANDBOX)}")
   return "n".be a part of(gadgets[:60]) or "(empty)"


@device
def python_exec(code: str) -> str:
   """Run Python within the sandbox. SANDBOX_ROOT is preset."""
   g = {"__name__":"__sb__", "SANDBOX_ROOT": str(SANDBOX)}
   buf = io.StringIO()
   strive:
       with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
           exec(code, g)
       return (buf.getvalue() or "(no stdout)")[:4000]
   besides Exception as e:
       return f"{kind(e).__name__}: {e}n{buf.getvalue()[:1500]}"


@device
def keep in mind(reality: str) -> str:
   """Persist a single reality to long-term reminiscence (survives throughout runs)."""
   m = _load_mem()
   m["facts"].append({"reality": reality, "ts": datetime.now(timezone.utc).isoformat()})
   _save_mem(m)
   return f"remembered ({len(m['facts'])} whole)"


@device
def recall() -> str:
   """Retrieve all the things in long-term reminiscence."""
   m = _load_mem()
   if not m["facts"]: return "(reminiscence empty)"
   return "n".be a part of(f"- {f['fact']}" for f in m["facts"][-20:])

We outline the primary instruments the Groq-backed agent can name throughout execution, together with itemizing abilities, loading ability directions, looking out the net, fetching webpages, studying recordsdata, and writing recordsdata. We additionally present the agent with a sandboxed Python execution atmosphere so it might probably run computations or generate artifacts when wanted. We add reminiscence instruments that enable the agent to recollect necessary information and recall beforehand saved info.

@device
def spawn_subagent(function: str, job: str,
                  allowed_tools: str = "web_search,web_fetch,file_write,file_read") -> str:
   """Spawn an remoted sub-agent with a centered function and scoped instruments.
   Returns its closing report string. Use for parallelizable / centered subtasks."""
   bag = {t.title: t for t in BASE_TOOLS}
   sub_tools = [bag[n.strip()] for n in allowed_tools.break up(",") if n.strip() in bag]
   sub_llm = ChatOpenAI(mannequin=MODEL_NAME, temperature=0.2).bind_tools(sub_tools)
   sys_msg = SystemMessage(content material=(
       f"You are a specialised sub-agent. Role: {function}.n"
       f"You function in an ISOLATED context — no entry to guide historical past.n"
       f"Tools: {', '.be a part of(t.title for t in sub_tools)}.n"
       "End with a closing assistant message beginning 'FINAL REPORT:' "
       "containing a structured ≤700-word abstract together with any URLs."))
   msgs: List[BaseMessage] = [sys_msg, HumanMessage(content=task)]
   for _ in vary(8):
       r = sub_llm.invoke(msgs); msgs.append(r)
       if not getattr(r, "tool_calls", None):
           return f"[sub-agent: {role}]n" + (r.content material if isinstance(r.content material,str) else str(r.content material))
       for tc in r.tool_calls:
           t = bag.get(tc["name"])
           strive:
               res = t.invoke(tc["args"]) if t else f"unknown device {tc['name']}"
           besides Exception as e:
               res = f"device error: {e}"
           msgs.append(ToolMessage(content material=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 format (relative to {SANDBOX}):
 uploads/    – consumer recordsdata
 workspace/  – your scratchpad
 outputs/    – closing deliverables
 abilities/     – functionality modules (load_skill)


Principles:
 • For non-trivial duties: list_skills → load_skill → execute.
 • Use spawn_subagent for centered subtasks (remoted context retains lead lean).
 • Persist intermediates to workspace/, deliverables to outputs/.
 • Use keep in mind(reality) for cross-session data.
 • Finish with a brief abstract of what was produced and the place.


Today: {datetime.now(timezone.utc).strftime('%Y-%m-%d')}."""


class AgentState(TypedDict):
   messages: Annotated[Sequence[BaseMessage], add_messages]


llm = ChatOpenAI(mannequin=MODEL_NAME, temperature=0.3).bind_tools(ALL_TOOLS)


def call_model(state: AgentState):
   msgs = record(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__"]:
   final = state["messages"][-1]
   return "instruments" if getattr(final, "tool_calls", None) else END


g = StateGraph(AgentState)
g.add_node("agent", call_model)
g.add_node("instruments", ToolNode(ALL_TOOLS))
g.set_entry_point("agent")
g.add_conditional_edges("agent", route, {"instruments":"instruments", END: END})
g.add_edge("instruments", "agent")
APP = g.compile()

We create a sub-agent device that enables the primary Groq-powered agent to delegate centered duties to an remoted assistant with a restricted set of instruments. We then gather all accessible instruments, outline the lead system immediate, initialize the Groq-backed chat mannequin, and bind the instruments to it. We lastly constructed the LangGraph workflow so the agent can alternate between reasoning and device execution till it reaches a closing reply.

def run(job: str, max_steps: int = 25):
   print("="*78); print(f"🦌 TASK: {job}"); 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.gadgets():
           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 material if isinstance(m.content material,str) else str(m.content material)
                       print(f"[{n:02}] 🦌 {txt[:800]}")
               elif isinstance(m, ToolMessage):
                   s = str(m.content material).substitute("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 reminiscence:"); 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 fashions (SLMs) in 2025. "
   "(1) uncover abilities; (2) spawn a researcher sub-agent to assemble "
   "specifics on three notable SLMs from 2024-2025 with sizes, benchmarks, "
   "and use instances — sub-agent saves to workspace/slm_research.md; "
   "(3) load report-generation ability and write outputs/slm_briefing.md "
   "(~400 phrases) with a Sources part; (4) save the only most "
   "necessary takeaway to long-term reminiscence; (5) summarize.",
   max_steps=25,
)

We outline the run() perform that begins a consumer job, streams every agent step, and prints device calls, device outputs, and closing responses in a readable format. We additionally show the sandbox file construction, long-term reminiscence, and generated output recordsdata after the workflow completes. We end by operating a demo job wherein the Groq-powered agent researches small language fashions, prepares a briefing, saves a report, and shops one key takeaway in reminiscence.

In conclusion, we created a compact but succesful Groq-based agent framework that demonstrates how Groq’s OpenAI-compatible API can function a quick, accessible backend for superior LLM workflows. We used LangGraph to handle the agent loop, LangChain to bind instruments to the Groq-hosted mannequin, and customized Python utilities to provide the system managed entry to look, recordsdata, code execution, and reminiscence. We additionally demonstrated how remoted sub-agents might help deal with centered analysis duties whereas the primary agent coordinates the general workflow. Also, we completed with a sensible Groq-powered agentic system that may be prolonged into analysis assistants, automated briefing turbines, and multi-step AI functions.


Check out the Full Codes with Notebook here. Also, be happy to comply with us on Twitter and don’t neglect to affix our 130k+ ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.

Need to companion with us for selling your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar and so forth.? Connect with us

The submit A Groq-Powered Agentic Research Assistant with LangGraph, Tool Calling, Sub-Agents, and Agentic Memory: Lets Built It appeared first on MarkTechPost.

Similar Posts