Multi-Agent Financial Research Assistant

Build a multi-agent AI system with RAG, compliance review, and observability.

Introduction

30 Second Summary

Financial institutions process thousands of research requests daily. Each one needs to pass through compliance checks before anyone sees it, because getting that wrong means regulatory fines or reputational damage.

In this project, you will build a multi-agent Financial Research Assistant that retrieves information from a knowledge base, synthesizes research findings, and routes them through an automated compliance review before presenting results.

What You'll Build

A LangGraph-powered multi-agent system with specialized Research and Compliance agents, RAG-based retrieval from a ChromaDB vector store, human-in-the-loop approval gates, and full execution tracing via LangSmith.

By the end of this project, you'll have:

  • A working multi-agent system with specialized Research and Compliance agents orchestrated by a LangGraph state machine.
  • A RAG pipeline that retrieves and synthesizes information from a financial knowledge base using ChromaDB.
  • Full observability of agent execution traces visible in LangSmith, with human-in-the-loop approval gates enforcing governance controls.
  • Secret Mission: Add a custom evaluation harness that scores agent outputs for accuracy and compliance, then logs results to LangSmith.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, if/else). No prior AI or machine learning experience is required.

Before We Start

Building a multi-agent system requires a few specialized tools working together. You need a modern version of Python, a vector database for your knowledge base, and API access to a large language model.

In this step, you will install Python 3.10+, set up a project directory with all required packages, and create free accounts for the two cloud services your agents will use.

In this step, get ready to:
  • Install Python 3.10+ (required by LangGraph).
  • Create a project directory with a virtual environment and install all dependencies.
  • Set up free accounts on Groq and LangSmith and configure your API keys.
Install Python 3.10+

LangGraph requires Python 3.10 or higher. Let's check what version you currently have installed.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.10 or higher

Your Python version is ready to go. Move on to the next substep.

ⓧ I see an older version

Your system has an older Python that won't work with LangGraph. You need to install Python 3.10+ alongside it.

  • Go to python.org/downloads and download the latest Python 3 installer for your operating system.
  • On Windows, run the installer and check Add python.exe to PATH before clicking Install Now.
  • On macOS, run the .pkg installer and follow the prompts.
  • Close and reopen your terminal, then run the version check command again.
  • If macOS still shows the old version, try python3.14 --version (using the version number you installed).

macOS PATH priority

macOS sometimes keeps pointing to the old Python even after installing a new one. The python.org installer adds the new version under /Library/Frameworks/Python.framework/Versions/.

If python3 --version still shows the old version, use the full path to the new binary (e.g. /Library/Frameworks/Python.framework/Versions/3.14/bin/python3).

ⓧ Command not found

Python is not installed on your system yet.

  • Go to python.org/downloads and download the latest Python 3 installer for your operating system.
  • On Windows, run the installer and check Add python.exe to PATH before clicking Install Now.
  • On macOS, run the .pkg installer and follow the prompts.
  • Close and reopen your terminal, then run the version check command again to confirm it worked.
Create a project directory and virtual environment

A virtual environment isolates your project's packages from the rest of your system. This prevents version conflicts between projects.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir financial-research-agent
cd financial-research-agent
  • Create and activate a virtual environment by running these commands:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

What does this do?

The python3 -m venv venv command creates a folder called venv that contains an isolated copy of Python and pip. Activating it tells your terminal to use this isolated Python for all subsequent commands.

You'll know it's active when you see (venv) at the beginning of your terminal prompt.

  • Install all the project packages by running this command:
pip install langgraph==1.2.5 langchain-groq==1.1.3 chromadb==1.5.9 langsmith

What are these packages?

  • langgraph orchestrates your multi-agent workflow as a state machine.
  • langchain-groq connects your agents to the Groq LLM API.
  • chromadb is a local vector database for your RAG knowledge base.
  • langsmith provides execution tracing and observability for your agents.

You should see pip downloading and installing each package along with its dependencies. This may take a minute or two.

Seeing errors during install?

  • If you see a permissions error, make sure your virtual environment is active. Check for (venv) at the start of your prompt.
  • If pip is not found, try pip3 instead.
  • If chromadb fails to build, make sure you have Python 3.10+ active (not your system's older Python).

Help me troubleshoot my package installation errors.

  • Confirm the packages installed by running:
pip list | grep -i langgraph

You should see langgraph with version 1.2.5 in the output.

Create free accounts and get API keys

Your agents need two external services. Groq provides the LLM that powers your agents' reasoning. LangSmith records execution traces so you can debug and monitor your agents.

Both services offer free tiers with no credit card required.

  • Go to console.groq.com and sign up for a free account.
  • Once signed in, navigate to API Keys in the left sidebar.
  • Click Create API Key.
  • Give it a name like financial-research and copy the key value immediately.
  • Store this key somewhere safe (a temporary note or password manager). You will not be able to see it again after leaving the page.

What is Groq?

Groq is an LLM inference provider that hosts open-source models like llama-3.3-70b-versatile. The free tier gives you 30 requests per minute and 1,000 requests per day for this model. That is more than enough for building and testing your agents.

  • Go to smith.langchain.com and sign up for a free account.
  • Once signed in, click your profile icon in the bottom left, then select Settings.
  • Find the API Keys section and create a new key.
  • Copy it immediately and store it safely.

What is LangSmith?

LangSmith is an observability platform for LLM applications. It records every step your agents take (LLM calls, retrieval results, decisions) as a trace you can inspect in a dashboard.

The free Developer plan includes 5,000 traces per month. No credit card is required.

Now create a .env file to store your keys securely outside your code.

  • In your terminal (still inside the financial-research-agent folder), create the .env file by running:

macOS

touch .env

Windows

New-Item .env
  • Open the .env file in VS Code by running:
code .env
  • Paste the following content into the file, replacing the placeholder values with your actual API keys:
GROQ_API_KEY=your-groq-api-key-here
LANGSMITH_API_KEY=your-langsmith-api-key-here
LANGSMITH_TRACING=true
LANGSMITH_PROJECT=financial-research-agent

What do these variables do?

  • GROQ_API_KEY authenticates your agents with the Groq LLM service.
  • LANGSMITH_API_KEY authenticates with LangSmith for trace recording.
  • LANGSMITH_TRACING=true enables automatic trace capture for every LangGraph run.
  • LANGSMITH_PROJECT groups all your traces under a named project in the LangSmith dashboard.
  • Save the .env file by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Can't open VS Code from the terminal?

If the code command is not recognized, open VS Code manually. Click File then Open Folder and select your financial-research-agent folder on the Desktop. Then create a new file named .env in the Explorer sidebar.

Help me open my project in VS Code.

✔️ Awesome, I've got everything!

Great. Double check you have saved the .env file with your actual API keys filled in.

ⓧ I'd like to double check the full code

Here is the complete .env file for reference. Make sure you have replaced the placeholder values with your actual keys.

GROQ_API_KEY=your-groq-api-key-here
LANGSMITH_API_KEY=your-langsmith-api-key-here
LANGSMITH_TRACING=true
LANGSMITH_PROJECT=financial-research-agent

Your environment is fully set up. Next up, you will build the RAG knowledge base that gives your Research Agent access to financial documents.

Build the RAG Knowledge Base

Your environment is ready and your API keys are configured. Now you need to solve the core problem: your LLM has no access to your organization's specific financial documents.

This is where RAG (Retrieval-Augmented Generation) comes in. You'll store financial documents in a vector store, then retrieve the most relevant ones based on the user's question before sending them to the LLM. In this step, you'll build that retrieval layer using ChromaDB.

In this step, get ready to:
  • Create sample financial documents covering regulations, risk management, and compliance.
  • Load documents into a ChromaDB vector collection with metadata tags.
  • Build and test a retrieval function that finds relevant documents by semantic similarity.
Create sample financial documents and load them into ChromaDB

A RAG pipeline needs a knowledge base to retrieve from. You'll create a set of financial documents covering topics like capital requirements, AML compliance, and market risk. Then you'll load them into ChromaDB so they can be searched by meaning, not just keywords.

  • Create a new file called knowledge_base.py in your financial-research-agent folder.
  • Add the following code to define your financial documents and the function that loads them into ChromaDB:
import chromadb

# Sample financial documents covering regulations, risk, and compliance
FINANCIAL_DOCUMENTS = [
    {
        "id": "doc1",
        "text": "Basel III requires banks to maintain a minimum Common Equity Tier 1 (CET1) capital ratio of 4.5% of risk-weighted assets. The total capital ratio must be at least 8%. Banks must also maintain a capital conservation buffer of 2.5%, bringing the total CET1 requirement to 7%.",
        "metadata": {"type": "regulation", "topic": "capital_requirements"},
    },
    {
        "id": "doc2",
        "text": "Anti-Money Laundering (AML) regulations require financial institutions to implement Customer Due Diligence (CDD) procedures. Enhanced Due Diligence (EDD) must be applied to high-risk customers, including politically exposed persons (PEPs) and customers from high-risk jurisdictions.",
        "metadata": {"type": "regulation", "topic": "aml_compliance"},
    },
    {
        "id": "doc3",
        "text": "Market risk in trading portfolios is measured using Value at Risk (VaR) models. The Basel Committee recommends a 99% confidence level with a 10-day holding period for regulatory capital calculations. Stressed VaR must also be calculated using a period of significant financial stress.",
        "metadata": {"type": "risk_management", "topic": "market_risk"},
    },
    {
        "id": "doc4",
        "text": "The FCA requires firms providing investment advice to ensure suitability of recommendations. Firms must obtain sufficient information about the client's knowledge, experience, financial situation, and investment objectives. All advice must be accompanied by appropriate risk disclaimers.",
        "metadata": {"type": "regulation", "topic": "investment_advice"},
    },
    {
        "id": "doc5",
        "text": "Operational risk encompasses losses from inadequate or failed internal processes, people, systems, or external events. The Basel framework provides three approaches for calculating operational risk capital: Basic Indicator Approach, Standardised Approach, and Advanced Measurement Approach.",
        "metadata": {"type": "risk_management", "topic": "operational_risk"},
    },
    {
        "id": "doc6",
        "text": "ESG (Environmental, Social, and Governance) disclosure requirements under SFDR mandate that financial market participants classify products as Article 6 (no sustainability focus), Article 8 (promotes environmental/social characteristics), or Article 9 (sustainable investment objective).",
        "metadata": {"type": "regulation", "topic": "esg_disclosure"},
    },
]


def create_knowledge_base():
    # Initialize an in-memory ChromaDB client
    client = chromadb.Client()
    collection = client.get_or_create_collection(
        name="financial_knowledge_base"
    )

    # Add all documents with their text and metadata to the collection
    collection.add(
        ids=[doc["id"] for doc in FINANCIAL_DOCUMENTS],
        documents=[doc["text"] for doc in FINANCIAL_DOCUMENTS],
        metadatas=[doc["metadata"] for doc in FINANCIAL_DOCUMENTS],
    )

    return collection

What does this code do?

This code sets up a local financial knowledge base with six documents covering real-world topics like Basel III capital requirements, AML compliance, and ESG disclosure rules.

  • FINANCIAL_DOCUMENTS is a list of dictionaries, each with an id, text (the document content), and metadata (tags for type and topic).
  • create_knowledge_base() initializes a ChromaDB in-memory client, creates a collection called financial_knowledge_base, and adds all six documents with their text and metadata.
  • ChromaDB automatically converts each document's text into a vector embedding for semantic search.
  • Save knowledge_base.py.

You won't see visible output yet. The next chunk adds the retrieval function that queries this data.

Why ChromaDB for a vector store?

ChromaDB runs entirely in-memory on your local machine. There's no external service to configure or pay for. You get the full RAG experience (embeddings, semantic search, metadata filtering) without cloud infrastructure costs.

Implement the retrieval function

Now you need a function that takes a user's question and finds the most relevant documents from the knowledge base. ChromaDB handles this through semantic similarity search, comparing the meaning of the query against each stored document.

  • In knowledge_base.py, add the following function below the create_knowledge_base() function:
def retrieve_documents(collection, query, n_results=3):
    # Query ChromaDB for the most semantically similar documents
    results = collection.query(
        query_texts=[query],
        n_results=n_results,
    )

    # Format results into a clean list of dictionaries
    documents = []
    for i in range(len(results["ids"][0])):
        documents.append(
            {
                "id": results["ids"][0][i],
                "text": results["documents"][0][i],
                "metadata": results["metadatas"][0][i],
                "distance": results["distances"][0][i],
            }
        )

    return documents

What does this code do?

  • collection.query() takes the user's question and finds the top n_results documents ranked by semantic similarity.
  • The function returns a list of dictionaries with each document's id, text, metadata, and distance (how far the document is from the query in vector space).
  • Lower distance means higher relevance. The Research Agent will use these retrieved documents as context for the LLM.
  • Save knowledge_base.py.

You won't see a change yet. You'll test this function in the next substep.

Test the retrieval function

Before wiring this into the agent graph, you should verify that the knowledge base returns relevant results. You'll add a test block that creates the collection, runs a sample query, and prints the results.

  • At the bottom of knowledge_base.py, add this test block:
if __name__ == "__main__":
    collection = create_knowledge_base()
    results = retrieve_documents(collection, "What are the capital requirements for banks?")
    print("Retrieved documents:")
    for doc in results:
        print(f"  [{doc['metadata']['topic']}] {doc['text'][:80]}...")

What does this code do?

  • The if __name__ == "__main__": block only runs when you execute the file directly, not when other files import from it.
  • It creates the knowledge base, queries it with a sample question about capital requirements, and prints the top 3 matching documents with their topics.
  • Save knowledge_base.py.
  • Run the script to test your knowledge base by running this command:
python knowledge_base.py
  • You should see output showing three retrieved documents. The capital_requirements document should appear first since it's the most relevant to the query.

Not seeing any output?

  • Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
  • Check that you're running the command from inside your financial-research-agent folder.
  • If you see ModuleNotFoundError: No module named 'chromadb', reinstall by running pip install chromadb==1.5.9.

Help me debug my knowledge_base.py script

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and you can see retrieved documents printing in your terminal.

ⓧ I'd like to double check the full code

Here's the complete knowledge_base.py file:

import chromadb

FINANCIAL_DOCUMENTS = [
    {
        "id": "doc1",
        "text": "Basel III requires banks to maintain a minimum Common Equity Tier 1 (CET1) capital ratio of 4.5% of risk-weighted assets. The total capital ratio must be at least 8%. Banks must also maintain a capital conservation buffer of 2.5%, bringing the total CET1 requirement to 7%.",
        "metadata": {"type": "regulation", "topic": "capital_requirements"},
    },
    {
        "id": "doc2",
        "text": "Anti-Money Laundering (AML) regulations require financial institutions to implement Customer Due Diligence (CDD) procedures. Enhanced Due Diligence (EDD) must be applied to high-risk customers, including politically exposed persons (PEPs) and customers from high-risk jurisdictions.",
        "metadata": {"type": "regulation", "topic": "aml_compliance"},
    },
    {
        "id": "doc3",
        "text": "Market risk in trading portfolios is measured using Value at Risk (VaR) models. The Basel Committee recommends a 99% confidence level with a 10-day holding period for regulatory capital calculations. Stressed VaR must also be calculated using a period of significant financial stress.",
        "metadata": {"type": "risk_management", "topic": "market_risk"},
    },
    {
        "id": "doc4",
        "text": "The FCA requires firms providing investment advice to ensure suitability of recommendations. Firms must obtain sufficient information about the client's knowledge, experience, financial situation, and investment objectives. All advice must be accompanied by appropriate risk disclaimers.",
        "metadata": {"type": "regulation", "topic": "investment_advice"},
    },
    {
        "id": "doc5",
        "text": "Operational risk encompasses losses from inadequate or failed internal processes, people, systems, or external events. The Basel framework provides three approaches for calculating operational risk capital: Basic Indicator Approach, Standardised Approach, and Advanced Measurement Approach.",
        "metadata": {"type": "risk_management", "topic": "operational_risk"},
    },
    {
        "id": "doc6",
        "text": "ESG (Environmental, Social, and Governance) disclosure requirements under SFDR mandate that financial market participants classify products as Article 6 (no sustainability focus), Article 8 (promotes environmental/social characteristics), or Article 9 (sustainable investment objective).",
        "metadata": {"type": "regulation", "topic": "esg_disclosure"},
    },
]


def create_knowledge_base():
    client = chromadb.Client()
    collection = client.get_or_create_collection(
        name="financial_knowledge_base"
    )

    collection.add(
        ids=[doc["id"] for doc in FINANCIAL_DOCUMENTS],
        documents=[doc["text"] for doc in FINANCIAL_DOCUMENTS],
        metadatas=[doc["metadata"] for doc in FINANCIAL_DOCUMENTS],
    )

    return collection


def retrieve_documents(collection, query, n_results=3):
    results = collection.query(
        query_texts=[query],
        n_results=n_results,
    )

    documents = []
    for i in range(len(results["ids"][0])):
        documents.append(
            {
                "id": results["ids"][0][i],
                "text": results["documents"][0][i],
                "metadata": results["metadatas"][0][i],
                "distance": results["distances"][0][i],
            }
        )

    return documents


if __name__ == "__main__":
    collection = create_knowledge_base()
    results = retrieve_documents(collection, "What are the capital requirements for banks?")
    print("Retrieved documents:")
    for doc in results:
        print(f"  [{doc['metadata']['topic']}] {doc['text'][:80]}...")

Your knowledge base is working. You can now retrieve relevant financial documents based on any natural language question. Next up, you'll build the specialized agents that use these retrieved documents to generate research reports and check them for compliance.

Create the Specialized Agents

Your knowledge base can retrieve relevant documents, but raw document chunks aren't useful to a user on their own. You need intelligent agents that can synthesize retrieved information into clear reports and review those reports for compliance.

In this step, you'll build two specialized agents. A Research Agent retrieves documents from ChromaDB and uses the Groq LLM to synthesize findings. A Compliance Agent evaluates outputs against regulatory rules and produces a structured approval or rejection.

In this step, get ready to:
  • Define a Research Agent that retrieves and synthesizes financial information.
  • Define a Compliance Agent that reviews outputs against regulatory rules.
  • Test both agents independently to verify they produce expected outputs.
Create the agents file with the LLM client

Both agents need access to a large language model to reason over documents and produce outputs. You'll initialize a shared ChatGroq client that both functions will use.

  • Create a new file called agents.py in your financial-research-agent folder.
  • Add the imports and LLM initialization by pasting this code:
import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq

# Load environment variables from .env file
load_dotenv()

# Initialize the Groq LLM client shared by both agents
llm = ChatGroq(
    model="llama-3.3-70b-versatile",
    temperature=0,
    api_key=os.getenv("GROQ_API_KEY"),
)

What does this code do?

load_dotenv() reads your .env file and loads the API keys into environment variables so your code can access them securely.

ChatGroq creates a connection to Groq's API using the llama-3.3-70b-versatile model. Setting temperature=0 makes the model's responses deterministic, which is important for a financial system that needs consistent, reproducible outputs.

  • Save agents.py.
  • Confirm the file exists by running:
ls agents.py

You should see agents.py listed in the output.

File not found?

Make sure you saved the file inside the financial-research-agent folder, not a subfolder or your Desktop. Your terminal should also be inside that folder.

help me find where my agents.py file was saved

Define the Research Agent

The Research Agent is the first specialized worker in your multi-agent system. It takes a user query and retrieved documents, then uses the LLM to synthesize them into a structured research report with citations.

  • In agents.py, add the research_agent function below the llm initialization:
def research_agent(query, retrieved_docs):
    # Build context string from retrieved documents with source tags
    context = "\n\n".join(
        [f"[Source: {doc['metadata']['topic']}] {doc['text']}" for doc in retrieved_docs]
    )

    # Construct a detailed prompt instructing the LLM to synthesize a report
    prompt = f"""You are a financial research analyst. Based on the following retrieved documents, 
synthesize a comprehensive research report answering the user's query.

Retrieved Documents:
{context}

User Query: {query}

Instructions:
- Cite sources using [Source: topic_name] format
- Structure your response with clear sections
- Include relevant regulatory references
- Be factual and precise

Research Report:"""

    response = llm.invoke(prompt)
    return response.content

What does this code do?

The function first formats retrieved documents into a context string, tagging each with its source topic. This tagging ensures the LLM can cite where each fact came from.

The prompt instructs the LLM to act as a financial research analyst. It provides the retrieved documents, the original query, and explicit instructions to cite sources and stay factual. This is the RAG pattern in action: retrieval feeds the LLM context it wouldn't otherwise have.

  • Save agents.py.

You won't see a change yet. The next substep adds the Compliance Agent, and then you'll test both together.

Define the Compliance Agent

In production financial systems, raw research outputs can't go directly to users. They need a compliance review to ensure regulatory rules are followed.

Your Compliance Agent receives a research report and evaluates it against a defined set of rules. It outputs a structured decision: approved or rejected, with reasoning and suggestions.

  • In agents.py, add the COMPLIANCE_RULES constant below the research_agent function:
COMPLIANCE_RULES = """
1. All research outputs must include source citations from the knowledge base.
2. Investment-related content must include a disclaimer: "This is for informational purposes only and does not constitute investment advice."
3. Risk assessments must reference specific regulatory frameworks (e.g., Basel III, FCA guidelines).
4. Outputs must not contain speculative language about future market performance without clearly labeling it as opinion.
5. Any mention of specific financial products must include associated risk warnings.
"""

What are these rules for?

These compliance rules mirror real-world regulatory requirements in financial services. Each rule gives the Compliance Agent a specific criterion to check against.

The rules are defined as a separate constant so they can be updated independently of the agent logic. In a production system, these might come from a database or policy management tool.

  • Now add the compliance_agent function directly below the COMPLIANCE_RULES constant:
def compliance_agent(research_report, query):
    # Prompt the LLM to act as a compliance reviewer
    prompt = f"""You are a compliance review agent for a financial institution. 
Review the following research report against the compliance rules below.

Compliance Rules:
{COMPLIANCE_RULES}

Research Report to Review:
{research_report}

Original Query: {query}

Evaluate the report and respond in this exact format:
DECISION: APPROVED or REJECTED
ISSUES: List any compliance issues found (or "None" if approved)
REASONING: Brief explanation of your decision
SUGGESTIONS: If rejected, specific changes needed to achieve compliance"""

    response = llm.invoke(prompt)
    return response.content

What does this code do?

The Compliance Agent receives the full research report and the original query for context. It uses a structured prompt that asks the LLM to evaluate the report against each compliance rule.

The output format (DECISION, ISSUES, REASONING, SUGGESTIONS) is explicitly defined in the prompt. This structured response format makes it possible for downstream code to parse the decision programmatically.

Why two separate agents instead of one?

Separating research from compliance follows the single-responsibility principle. Each agent has one job and one set of instructions, which makes outputs more predictable and easier to debug.

In a multi-agent system, specialized agents also make it easy to swap, upgrade, or add new review stages without rewriting the entire pipeline.

  • Save agents.py.

You won't see output yet. The next substep adds a test block that runs both agents.

Test both agents independently

Before wiring agents into a larger graph, you should verify each one works on its own. This test block calls both agents in sequence and prints their outputs so you can inspect the results.

  • At the bottom of agents.py, add this test block:
if __name__ == "__main__":
    from knowledge_base import create_knowledge_base, retrieve_documents

    # Set up knowledge base and retrieve relevant docs
    collection = create_knowledge_base()
    query = "What are the capital requirements under Basel III?"
    docs = retrieve_documents(collection, query)

    # Run the Research Agent
    print("=== Research Agent Output ===")
    report = research_agent(query, docs)
    print(report)

    # Run the Compliance Agent on the research output
    print("\n=== Compliance Agent Output ===")
    review = compliance_agent(report, query)
    print(review)

What does this test do?

The if __name__ == "__main__" block only runs when you execute this file directly. It won't run when other files import from agents.py.

The test creates the knowledge base, retrieves documents about Basel III capital requirements, feeds them to the Research Agent, then passes that report to the Compliance Agent for review. This mirrors exactly how the full pipeline will work.

  • Save agents.py.
  • Run the test by executing:
python agents.py

You should see two sections of output. The Research Agent output is a synthesized report about Basel III capital requirements with [Source: capital_requirements] citations. The Compliance Agent output shows a structured DECISION (APPROVED or REJECTED) with ISSUES, REASONING, and SUGGESTIONS.

Seeing a rate limit or authentication error?

If you see a 401 error, double-check that your GROQ_API_KEY in .env is correct and has no extra spaces around it.

If you see a 429 rate limit error, wait 60 seconds and try again. The Groq free tier allows 30 requests per minute for this model.

help me debug my Groq API connection error when running agents.py

✔️ Awesome, I've got everything!

Great. Double check you've saved agents.py and both agent outputs printed successfully.

ⓧ I'd like to double check the full code

Here is the complete agents.py file at this point:

import os
from dotenv import load_dotenv
from langchain_groq import ChatGroq

load_dotenv()

llm = ChatGroq(
    model="llama-3.3-70b-versatile",
    temperature=0,
    api_key=os.getenv("GROQ_API_KEY"),
)

COMPLIANCE_RULES = """
1. All research outputs must include source citations from the knowledge base.
2. Investment-related content must include a disclaimer: "This is for informational purposes only and does not constitute investment advice."
3. Risk assessments must reference specific regulatory frameworks (e.g., Basel III, FCA guidelines).
4. Outputs must not contain speculative language about future market performance without clearly labeling it as opinion.
5. Any mention of specific financial products must include associated risk warnings.
"""


def research_agent(query, retrieved_docs):
    context = "\n\n".join(
        [f"[Source: {doc['metadata']['topic']}] {doc['text']}" for doc in retrieved_docs]
    )

    prompt = f"""You are a financial research analyst. Based on the following retrieved documents, 
synthesize a comprehensive research report answering the user's query.

Retrieved Documents:
{context}

User Query: {query}

Instructions:
- Cite sources using [Source: topic_name] format
- Structure your response with clear sections
- Include relevant regulatory references
- Be factual and precise

Research Report:"""

    response = llm.invoke(prompt)
    return response.content


def compliance_agent(research_report, query):
    prompt = f"""You are a compliance review agent for a financial institution. 
Review the following research report against the compliance rules below.

Compliance Rules:
{COMPLIANCE_RULES}

Research Report to Review:
{research_report}

Original Query: {query}

Evaluate the report and respond in this exact format:
DECISION: APPROVED or REJECTED
ISSUES: List any compliance issues found (or "None" if approved)
REASONING: Brief explanation of your decision
SUGGESTIONS: If rejected, specific changes needed to achieve compliance"""

    response = llm.invoke(prompt)
    return response.content


if __name__ == "__main__":
    from knowledge_base import create_knowledge_base, retrieve_documents

    collection = create_knowledge_base()
    query = "What are the capital requirements under Basel III?"
    docs = retrieve_documents(collection, query)

    print("=== Research Agent Output ===")
    report = research_agent(query, docs)
    print(report)

    print("\n=== Compliance Agent Output ===")
    review = compliance_agent(report, query)
    print(review)

Both agents are working independently. Next up, you'll wire them together into a LangGraph state machine that orchestrates the full pipeline with conditional routing and human-in-the-loop approval.

Orchestrate with LangGraph and Human-in-the-Loop

Your Research Agent and Compliance Agent both work independently. But in a production system, you need something to coordinate them.

In this step, you will wire both agents into a LangGraph state machine that controls the flow of data between them. The graph will route outputs based on the compliance decision and pause execution for human approval before delivering the final result.

In this step, get ready to:
  • Define a shared state schema for data flow between agents.
  • Build a LangGraph state machine with conditional routing.
  • Add human-in-the-loop governance using interrupt gates.
Define the graph state and build node functions

A state machine needs a shared data structure that every node can read from and write to. In LangGraph, you define this structure using Python's TypedDict.

  • Create a new file called graph.py in your financial-research-agent folder.
  • Add the imports and state definition by pasting this code:
import os
from dotenv import load_dotenv
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt

from knowledge_base import create_knowledge_base, retrieve_documents
from agents import research_agent, compliance_agent

load_dotenv()


# Define the shared state that flows between all nodes
class AgentState(TypedDict):
    query: str
    retrieved_docs: list
    research_report: str
    compliance_decision: str
    compliance_reasoning: str
    final_output: str
    revision_count: int

What does this code do?

The AgentState class defines every piece of data the graph will track as it moves from node to node. Each field maps to a stage in the pipeline: the original query, the retrieved_docs from ChromaDB, the synthesized research_report, and the compliance review outputs.

The revision_count field tracks how many times the research agent has revised its output after a compliance rejection. This prevents infinite loops.

Next, you need node functions that actually do the work at each stage. Each node receives the current state and returns a dictionary of updates.

  • Add the retrieval and research node functions below the AgentState class:
def retrieve_node(state: AgentState) -> dict:
    # Query the ChromaDB knowledge base for relevant documents
    collection = create_knowledge_base()
    docs = retrieve_documents(collection, state["query"])
    return {"retrieved_docs": docs}


def research_node(state: AgentState) -> dict:
    # Use the Research Agent to synthesize findings from retrieved docs
    report = research_agent(state["query"], state["retrieved_docs"])
    return {"research_report": report}

What does this code do?

Each node function takes the full AgentState and returns only the fields it updates. LangGraph merges these updates into the shared state automatically.

The retrieve_node populates retrieved_docs from the knowledge base. The research_node calls your existing research_agent() function and writes the result into research_report.

Now add the compliance node, which parses the agent's output to extract a decision.

  • Add the compliance node function below research_node:
def compliance_node(state: AgentState) -> dict:
    # Run the Compliance Agent to review the research report
    review = compliance_agent(state["research_report"], state["query"])

    # Parse the decision from the compliance agent's response
    decision = "APPROVED"
    if "DECISION: REJECTED" in review:
        decision = "REJECTED"

    return {
        "compliance_decision": decision,
        "compliance_reasoning": review,
        "revision_count": state.get("revision_count", 0) + 1,
    }

What does this code do?

The compliance_node calls your existing compliance_agent() function and parses the text response for a structured decision. It checks whether the response contains DECISION: REJECTED to determine the routing outcome.

It also increments revision_count each time it runs. This counter acts as a circuit breaker so the graph does not loop endlessly between research and compliance.

  • Save graph.py.

Seeing an import error?

Make sure knowledge_base.py and agents.py are in the same folder as graph.py. Python needs them in the same directory to resolve the imports.

help me debug an import error in my graph.py file

Add conditional routing and human-in-the-loop

In financial services, no AI output should reach the end user without human oversight. LangGraph's interrupt() function pauses the graph and waits for a human decision before continuing.

  • Add the human approval node and output node below compliance_node:
def human_approval_node(state: AgentState) -> dict:
    # Display the report and compliance decision for human review
    print("\n" + "=" * 60)
    print("HUMAN-IN-THE-LOOP: Review Required")
    print("=" * 60)
    print(f"\nResearch Report:\n{state['research_report'][:500]}...")
    print(f"\nCompliance Decision: {state['compliance_decision']}")
    print(f"\nCompliance Reasoning:\n{state['compliance_reasoning'][:300]}...")

    # Pause the graph and wait for human input
    answer = interrupt(
        {"question": "Do you approve this output? (yes/no)", "report_preview": state["research_report"][:200]}
    )

    if answer.lower() == "yes":
        return {"final_output": state["research_report"]}
    else:
        return {"final_output": "OUTPUT REJECTED BY HUMAN REVIEWER"}


def output_node(state: AgentState) -> dict:
    # Append a compliance disclaimer to the final output
    disclaimer = "\n\n---\nDisclaimer: This is for informational purposes only and does not constitute investment advice."
    return {"final_output": state["research_report"] + disclaimer}

How does interrupt() work?

When the graph reaches interrupt(), it saves its current state and pauses execution completely. The graph stays paused until you resume it with an answer. The answer becomes the return value of interrupt() inside the node.

This requires a checkpointer (you will add one shortly) so LangGraph can persist the state while waiting. Without a checkpointer, the interrupt has nowhere to save the paused state.

Now you need the routing logic that decides where to send the graph after compliance review.

  • Add the routing function below output_node:
def route_after_compliance(state: AgentState) -> Literal["human_approval", "research"]:
    # If approved, send to human for final sign-off
    if state["compliance_decision"] == "APPROVED":
        return "human_approval"
    # If rejected twice, escalate to human anyway
    if state.get("revision_count", 0) >= 2:
        return "human_approval"
    # Otherwise, loop back to research for revision
    return "research"

What is conditional routing?

Instead of fixed connections between nodes, conditional edges let the graph choose its path at runtime. This function returns a string matching a node name, and LangGraph follows that path.

The logic here implements three scenarios: approved reports go to human approval, rejected reports loop back to research, and reports rejected twice escalate to human review regardless. This prevents the system from looping forever on an edge case.

Finally, wire everything together into a reusable build_graph() function.

  • Add the build_graph function below route_after_compliance:
def build_graph():
    # Create the state machine with our state schema
    graph = StateGraph(AgentState)

    # Register each node function
    graph.add_node("retrieve", retrieve_node)
    graph.add_node("research", research_node)
    graph.add_node("compliance", compliance_node)
    graph.add_node("human_approval", human_approval_node)
    graph.add_node("output", output_node)

    # Define the edges (flow between nodes)
    graph.add_edge(START, "retrieve")
    graph.add_edge("retrieve", "research")
    graph.add_edge("research", "compliance")
    graph.add_conditional_edges(
        "compliance",
        route_after_compliance,
        {"human_approval": "human_approval", "research": "research"},
    )
    graph.add_edge("human_approval", "output")
    graph.add_edge("output", END)

    return graph.compile()

What does this code do?

The build_graph() function assembles the full state machine. It registers nodes, connects them with edges, and compiles the graph into a runnable application.

The add_conditional_edges call connects the compliance node to route_after_compliance, mapping each possible return value to a target node. The START and END constants mark where the graph begins and terminates.

  • Save graph.py.

Seeing a NameError for a node function?

Make sure all five node functions (retrieve_node, research_node, compliance_node, human_approval_node, output_node) are defined above the build_graph() function. Python reads top to bottom, so functions must exist before they are referenced.

help me fix a NameError in my graph.py

Compile and run the graph end-to-end

The build_graph() function compiles a graph without a checkpointer, which means it cannot persist state across interrupts. To test the full human-in-the-loop flow, you need an InMemorySaver checkpointer and a thread ID.

  • Add the following __main__ block at the bottom of graph.py:
if __name__ == "__main__":
    from langgraph.checkpoint.memory import InMemorySaver

    # Rebuild the graph with a checkpointer for interrupt support
    graph = StateGraph(AgentState)

    graph.add_node("retrieve", retrieve_node)
    graph.add_node("research", research_node)
    graph.add_node("compliance", compliance_node)
    graph.add_node("human_approval", human_approval_node)
    graph.add_node("output", output_node)

    graph.add_edge(START, "retrieve")
    graph.add_edge("retrieve", "research")
    graph.add_edge("research", "compliance")
    graph.add_conditional_edges(
        "compliance",
        route_after_compliance,
        {"human_approval": "human_approval", "research": "research"},
    )
    graph.add_edge("human_approval", "output")
    graph.add_edge("output", END)

    # Compile with checkpointer so interrupt() can save state
    checkpointer = InMemorySaver()
    app = graph.compile(checkpointer=checkpointer)

    # Configure a unique thread for this session
    config = {"configurable": {"thread_id": "research-session-1"}}
    initial_state = {
        "query": "What are the capital requirements under Basel III and how do they relate to risk management?",
        "retrieved_docs": [],
        "research_report": "",
        "compliance_decision": "",
        "compliance_reasoning": "",
        "final_output": "",
        "revision_count": 0,
    }

    print("Starting multi-agent financial research assistant...")
    print(f"Query: {initial_state['query']}\n")

    # Stream the graph until it hits the interrupt
    for event in app.stream(initial_state, config=config):
        for node_name, node_output in event.items():
            print(f"[Node: {node_name}] completed")

    # Check if the graph paused at the human approval interrupt
    state = app.get_state(config)
    if state.next:
        print(f"\nGraph paused at: {state.next}")
        print("Resuming with human approval...")
        for event in app.stream({"query": initial_state["query"], "answer": "yes"}, config=config):
            for node_name, node_output in event.items():
                print(f"[Node: {node_name}] completed")

    # Print the final output
    final_state = app.get_state(config)
    print("\n" + "=" * 60)
    print("FINAL OUTPUT:")
    print("=" * 60)
    if final_state.values.get("final_output"):
        print(final_state.values["final_output"])

What does this code do?

The __main__ block rebuilds the graph with an InMemorySaver checkpointer. This is required because interrupt() needs a persistence layer to save the graph state while waiting for human input.

The thread_id identifies this execution session. After the first app.stream() call runs until the interrupt, app.get_state(config) checks whether the graph is paused. If it is, the code resumes by streaming again with the same config, simulating a human approving the output.

  • Save graph.py.
  • Run the full pipeline by executing this command in your terminal:
python graph.py
  • You should see output showing each node completing in sequence: retrieve, research, compliance, then the graph pausing at the human approval interrupt before resuming and printing the final research report with a disclaimer appended.

Graph not pausing at the interrupt?

Make sure you are compiling the graph with checkpointer=checkpointer in the __main__ block. Without a checkpointer, interrupt() cannot save state and the graph will not pause correctly.

Also check that your .env file contains a valid GROQ_API_KEY. The research and compliance nodes call the Groq LLM, so an invalid key will cause an authentication error before reaching the interrupt.

help me debug my LangGraph interrupt not working

✔️ Awesome, I've got everything!

Great. Double check you have saved graph.py and that the pipeline ran successfully.

ⓧ I'd like to double check the full code

import os
from dotenv import load_dotenv
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt

from knowledge_base import create_knowledge_base, retrieve_documents
from agents import research_agent, compliance_agent

load_dotenv()


class AgentState(TypedDict):
    query: str
    retrieved_docs: list
    research_report: str
    compliance_decision: str
    compliance_reasoning: str
    final_output: str
    revision_count: int


def retrieve_node(state: AgentState) -> dict:
    collection = create_knowledge_base()
    docs = retrieve_documents(collection, state["query"])
    return {"retrieved_docs": docs}


def research_node(state: AgentState) -> dict:
    report = research_agent(state["query"], state["retrieved_docs"])
    return {"research_report": report}


def compliance_node(state: AgentState) -> dict:
    review = compliance_agent(state["research_report"], state["query"])

    decision = "APPROVED"
    if "DECISION: REJECTED" in review:
        decision = "REJECTED"

    return {
        "compliance_decision": decision,
        "compliance_reasoning": review,
        "revision_count": state.get("revision_count", 0) + 1,
    }


def human_approval_node(state: AgentState) -> dict:
    print("\n" + "=" * 60)
    print("HUMAN-IN-THE-LOOP: Review Required")
    print("=" * 60)
    print(f"\nResearch Report:\n{state['research_report'][:500]}...")
    print(f"\nCompliance Decision: {state['compliance_decision']}")
    print(f"\nCompliance Reasoning:\n{state['compliance_reasoning'][:300]}...")

    answer = interrupt(
        {"question": "Do you approve this output? (yes/no)", "report_preview": state["research_report"][:200]}
    )

    if answer.lower() == "yes":
        return {"final_output": state["research_report"]}
    else:
        return {"final_output": "OUTPUT REJECTED BY HUMAN REVIEWER"}


def output_node(state: AgentState) -> dict:
    disclaimer = "\n\n---\nDisclaimer: This is for informational purposes only and does not constitute investment advice."
    return {"final_output": state["research_report"] + disclaimer}


def route_after_compliance(state: AgentState) -> Literal["human_approval", "research"]:
    if state["compliance_decision"] == "APPROVED":
        return "human_approval"
    if state.get("revision_count", 0) >= 2:
        return "human_approval"
    return "research"


def build_graph():
    graph = StateGraph(AgentState)

    graph.add_node("retrieve", retrieve_node)
    graph.add_node("research", research_node)
    graph.add_node("compliance", compliance_node)
    graph.add_node("human_approval", human_approval_node)
    graph.add_node("output", output_node)

    graph.add_edge(START, "retrieve")
    graph.add_edge("retrieve", "research")
    graph.add_edge("research", "compliance")
    graph.add_conditional_edges(
        "compliance",
        route_after_compliance,
        {"human_approval": "human_approval", "research": "research"},
    )
    graph.add_edge("human_approval", "output")
    graph.add_edge("output", END)

    return graph.compile()


if __name__ == "__main__":
    from langgraph.checkpoint.memory import InMemorySaver

    graph = StateGraph(AgentState)

    graph.add_node("retrieve", retrieve_node)
    graph.add_node("research", research_node)
    graph.add_node("compliance", compliance_node)
    graph.add_node("human_approval", human_approval_node)
    graph.add_node("output", output_node)

    graph.add_edge(START, "retrieve")
    graph.add_edge("retrieve", "research")
    graph.add_edge("research", "compliance")
    graph.add_conditional_edges(
        "compliance",
        route_after_compliance,
        {"human_approval": "human_approval", "research": "research"},
    )
    graph.add_edge("human_approval", "output")
    graph.add_edge("output", END)

    checkpointer = InMemorySaver()
    app = graph.compile(checkpointer=checkpointer)

    config = {"configurable": {"thread_id": "research-session-1"}}
    initial_state = {
        "query": "What are the capital requirements under Basel III and how do they relate to risk management?",
        "retrieved_docs": [],
        "research_report": "",
        "compliance_decision": "",
        "compliance_reasoning": "",
        "final_output": "",
        "revision_count": 0,
    }

    print("Starting multi-agent financial research assistant...")
    print(f"Query: {initial_state['query']}\n")

    for event in app.stream(initial_state, config=config):
        for node_name, node_output in event.items():
            print(f"[Node: {node_name}] completed")

    state = app.get_state(config)
    if state.next:
        print(f"\nGraph paused at: {state.next}")
        print("Resuming with human approval...")
        for event in app.stream({"query": initial_state["query"], "answer": "yes"}, config=config):
            for node_name, node_output in event.items():
                print(f"[Node: {node_name}] completed")

    final_state = app.get_state(config)
    print("\n" + "=" * 60)
    print("FINAL OUTPUT:")
    print("=" * 60)
    if final_state.values.get("final_output"):
        print(final_state.values["final_output"])

Your multi-agent system is now fully orchestrated. The graph retrieves documents, synthesizes research, reviews for compliance, pauses for human approval, and delivers the final output. Next up, you will connect LangSmith to observe every step of this pipeline in real time.

Add Observability with LangSmith

Your multi-agent pipeline is fully functional. Queries flow through retrieval, research, compliance review, and human approval. But in a production financial system, you need to see exactly what happened inside each execution.

In this step, you'll confirm your LangSmith tracing configuration and run the agent to generate traces. Then you'll explore the trace view to inspect every node's inputs, outputs, LLM calls, token usage, and latency.

In this step, get ready to:
  • Confirm LangSmith tracing is configured and run the agent to generate traces.
  • Explore the LangSmith trace view to inspect the full agent execution path.
Confirm LangSmith tracing configuration

When you built your environment in Step 1, you already added the LangSmith environment variables to your .env file. These three variables control tracing behavior.

  • Switch back to VS Code and open your .env file.
  • Confirm it contains these three LangSmith variables:
LANGSMITH_API_KEY=your-langsmith-api-key-here
LANGSMITH_TRACING=true
LANGSMITH_PROJECT=financial-research-agent

What do these variables do?

  • LANGSMITH_TRACING=true tells the LangChain ecosystem to send execution data to LangSmith automatically.
  • LANGSMITH_API_KEY authenticates your requests with the LangSmith service.
  • LANGSMITH_PROJECT=financial-research-agent groups all your traces under a named project so they're easy to find in the dashboard.

Your graph.py already calls load_dotenv() at the top, which loads these variables into the process environment. No code changes are needed.

  • Make sure your virtual environment is active in the terminal.
  • Run the agent pipeline to generate your first traced execution:
python graph.py

You should see the familiar node completions printing in your terminal, ending with the final research output.

What happens behind the scenes?

Because LANGSMITH_TRACING=true is set, every LLM call, node execution, and state transition is automatically captured and sent to LangSmith. LangGraph has built-in tracing support that requires no extra code.

  • Open your browser and navigate to smith.langchain.com.
  • Sign in with the account you created in Step 1.
  • Click Tracing in the left sidebar.
  • Select the financial-research-agent project from the project list.

You should see at least one trace row appear in the table. This confirms tracing is working.

Don't see any traces?

  • Double check that LANGSMITH_TRACING=true is spelled exactly as shown (no spaces around the =).
  • Make sure your LANGSMITH_API_KEY is a valid key from your LangSmith account settings. If the key is wrong, traces are silently dropped.
  • Confirm the project name in your .env matches what you see in the LangSmith dashboard. If they differ, your traces might be under a different project.

Help me troubleshoot why my LangSmith traces aren't appearing in the dashboard.

Explore the trace view to inspect execution details

A trace captures the full execution path of your multi-agent system. Each node, each LLM call, and each state transition is recorded with timing and token data.

  • In the LangSmith project view, click on the most recent trace row to open it.
  • Toggle to the Details view using the button at the top right of the side panel.

You should see a run tree showing your graph's execution. Each node appears as a nested span inside the trace.

What should I look for in the Details view?

The Details view is your debugging layer. It shows inputs, outputs, timing, token counts, errors, and metadata for each run. Look for these elements:

  • The outer span captures your full graph execution from start to finish.
  • Nested spans show each node (retrieve, research, compliance, human_approval, output) with their individual inputs and outputs.
  • LLM call spans (inside the research and compliance nodes) show the exact prompt sent and the response returned, plus token counts and latency.
  • The retrieval span shows which documents were returned from ChromaDB.
  • Click on the research node span in the run tree to expand it.
  • Find the nested LLM call span inside. Click it to see the full prompt and response.
  • Note the token count and latency displayed for this LLM call.
  • Click on the compliance node span to see how the compliance review evaluated the research report.

Why does observability matter for financial AI?

In regulated industries, you need an audit trail of every decision an AI system makes. LangSmith traces provide exactly this. You can prove which documents were retrieved, what prompts were sent, and why the compliance agent approved or rejected a report.

Traces also help with debugging. If the compliance agent rejects a report unexpectedly, you can inspect the exact inputs it received and the reasoning it produced.

  • Look at the latency values for each node. Identify which node takes the longest (typically the LLM calls in research and compliance).
  • Check the total token usage across both LLM calls to understand the cost per query.

Keep an eye on rate limits

The Groq free tier allows 30 requests per minute and 1,000 requests per day for llama-3.3-70b-versatile. Each graph run makes at least 2 LLM calls (research + compliance). If you hit rate limits during testing, wait a minute before running again.

Your multi-agent system now has full observability. Every execution is recorded with enough detail to debug failures, satisfy compliance audits, and optimize performance. Next up, you'll tackle the Secret Mission and build a custom evaluation harness to score your agent's outputs.

Secret mission

Build a Custom Evaluation Harness

Your multi-agent system works end-to-end. But how do you know the outputs are actually good? In this bonus challenge, you'll build an LLM-as-judge evaluation harness that scores your agent's research reports across five quality dimensions and produces a summary report.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with free-tier cloud services, so there are no ongoing costs. Decide whether to keep your resources as-is, pause them to come back later, or delete them entirely.

Resources you used:

  • financial-research-agent project folder on your Desktop (includes all Python files, .env with API keys, venv virtual environment, and local ChromaDB data).
  • Groq API key (free tier, no charges possible without adding a payment method).
  • LangSmith API key (free Developer plan, 5,000 traces/month included, no credit card required).

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend the project soon.

  • Your Groq account is free and will never incur charges unless you explicitly add a payment method.
  • Your LangSmith Developer plan is free with 5,000 traces per month. No credit card is on file.
  • ChromaDB data is stored locally inside your project folder. It takes up minimal disk space.
  • The virtual environment stays on disk but does not use memory unless activated.

Pause - I'll come back to this later

Deactivate your virtual environment to free up terminal context. Your files and accounts stay intact for when you return.

  • If your virtual environment is still active, deactivate it by running:
  • No cloud resources to stop. Both Groq and LangSmith free tiers have no running processes or idle charges.
deactivate

When you come back, navigate to your project folder and reactivate the virtual environment.

Delete - I don't want to use this again

Remove all local project files and revoke API access. Your free-tier accounts will remain (they cost nothing) but the keys will no longer work.

  • Deactivate your virtual environment if it is still active:
deactivate
  • Delete the entire project folder from your Desktop by running this command (macOS/Linux):
cd ~/Desktop
rm -rf financial-research-agent
  • On Windows, run this instead:
cd ~\Desktop
rmdir /s /q financial-research-agent

This removes all Python files, the .env containing your API keys, the virtual environment, and any local ChromaDB data.

  • To revoke your Groq API key, go to console.groq.com/keys and delete the key you created for this project.
  • To revoke your LangSmith API key, open the Settings page in LangSmith and delete the key using the trash icon in the Actions column.

What about my accounts?

Both your Groq and LangSmith accounts are completely free. They will never charge you without you explicitly adding a payment method. You can leave them active for future projects at no cost.

Nice Work!

Nice Work!

You just built a production-patterned multi-agent Financial Research Assistant from scratch. Your system retrieves knowledge, synthesizes research, enforces compliance, and pauses for human approval before delivering results.

You've learned how to:

  • Built a multi-agent system with specialized Research and Compliance agents powered by RAG retrieval from a ChromaDB vector store.
  • Orchestrated agents using a LangGraph state machine with conditional routing and revision loops.
  • Added human-in-the-loop governance gates and full execution observability through LangSmith tracing.
  • Secret Mission: Created a custom LLM-as-judge evaluation harness that scores agent outputs across multiple quality dimensions.

Ready to quiz yourself?