Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ Thumbs.db
__pycache__/
*.py[cod]
*$py.class
uv.lock

# C extensions
*.so
Expand Down
1 change: 0 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
__pycache__/
*.py[cod]
*$py.class
uv.lock

# C extensions
*.so
Expand Down
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies = [
"langfuse",
"numpy<2.0.0",
"pycares<5.0.0",
"aiofiles>=25.1.0",
]


Expand Down
58 changes: 36 additions & 22 deletions backend/scripts/benchmark.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
"""Benchmark Orchestration Script

This script runs the agent against a dataset of questions and evaluates performance
This script runs the agent against a dataset of questions and evaluates performance
using the evaluators defined in backend/tests/evaluators.py.
"""

import asyncio
import logging
import json
import logging
import os
from typing import List, Dict, Any
from typing import Any, Dict, List

from dotenv import load_dotenv

# Load env vars before importing evaluators or agent components
load_dotenv()

from agent.graph import graph

try:
from tests.evaluators import eval_quality, eval_groundedness
from tests.evaluators import eval_groundedness, eval_quality
except ImportError:
# This might happen if running script directly without module context
# But usually handled by running as `python -m scripts.benchmark`
Expand All @@ -28,6 +30,7 @@
# Path relative to backend root
DATASET_PATH = os.path.join("tests", "data", "benchmark_questions.json")


def load_dataset(path: str) -> List[Dict[str, Any]]:
"""Load questions from a JSON file."""
if not os.path.exists(path):
Expand All @@ -41,12 +44,13 @@
return []

try:
with open(path, "r", encoding="utf-8") as f:
with open(path, encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load dataset: {e}")
return []


async def run_benchmark():
"""Run evaluation for all questions."""
questions = load_dataset(DATASET_PATH)
Expand All @@ -71,12 +75,13 @@
# For automation, we assume the graph can run autonomously or we'd need to mock input.
# Increase recursion limit to handle multi-step research plans (default is 25)
# Disable planning confirmation to allow automated execution
response = await graph.ainvoke({
"messages": [("user", question)]
}, config={
"recursion_limit": 100,
"configurable": {"require_planning_confirmation": False}
})
response = await graph.ainvoke(
{"messages": [("user", question)]},
config={
"recursion_limit": 100,
"configurable": {"require_planning_confirmation": False},
},
)

# Extract final answer from the last message content
messages = response.get("messages", [])
Expand Down Expand Up @@ -113,17 +118,25 @@
"question": question,
"expected_topics": expected_topics,
"quality_score": quality_result.get("score", 0),
"quality_reasoning": quality_result.get("metadata", {}).get("reasoning", "No reasoning provided"),
"quality_reasoning": quality_result.get("metadata", {}).get(
"reasoning", "No reasoning provided"
),
"groundedness_score": groundedness_result.get("score", 0),
"groundedness_reasoning": groundedness_result.get("metadata", {}).get("reasoning", "No reasoning provided"),
"final_answer_snippet": (final_content[:200] + "...") if final_content else "No content"
"groundedness_reasoning": groundedness_result.get("metadata", {}).get(
"reasoning", "No reasoning provided"
),
"final_answer_snippet": (final_content[:200] + "...")
if final_content
else "No content",
}
results.append(result_entry)

logger.info(f"Result for '{question}': Q={result_entry['quality_score']}, G={result_entry['groundedness_score']}")
logger.info(
f"Result Q={result_entry['quality_score']}, G={result_entry['groundedness_score']} for question"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

except Exception as e:

Check warning on line 138 in backend/scripts/benchmark.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused local variable "e".

See more on https://sonarcloud.io/project/issues?id=MasumRab_gemini-fullstack-langgraph-quickstart&issues=AZ4Hsa-6QHY4M-TJ2k4q&open=AZ4Hsa-6QHY4M-TJ2k4q&pullRequest=358
logger.error(f"Agent failed for '{question}': {e}", exc_info=True)
logger.error("Agent failed for question", exc_info=True)
continue

# Report Generation
Expand All @@ -141,12 +154,12 @@
"""
for r in results:
report += f"""
### {r['question']}
- **Quality:** {r['quality_score']}
- *Reasoning:* {r['quality_reasoning']}
- **Groundedness:** {r['groundedness_score']}
- *Reasoning:* {r['groundedness_reasoning']}
- **Snippet:** {r['final_answer_snippet']}
### {r["question"]}
- **Quality:** {r["quality_score"]}
- *Reasoning:* {r["quality_reasoning"]}
- **Groundedness:** {r["groundedness_score"]}
- *Reasoning:* {r["groundedness_reasoning"]}
- **Snippet:** {r["final_answer_snippet"]}
"""
print(report)

Expand All @@ -157,5 +170,6 @@
else:
logger.warning("No results to report.")


if __name__ == "__main__":
asyncio.run(run_benchmark())
5 changes: 3 additions & 2 deletions backend/scripts/check_path.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@

import sys
import os
import sys

print(sys.path)
try:
import agent

print(f"Agent: {agent}")
except ImportError as e:
print(f"ImportError: {e}")
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
import os
import sys
import asyncio
import json
import os
import sys
from datetime import datetime
from pathlib import Path

Expand All @@ -13,9 +13,10 @@

# Import Agent Components
try:
from agent.graph import graph
from agent.configuration import Configuration
from langchain_core.messages import HumanMessage

from agent.configuration import Configuration
from agent.graph import graph
except ImportError as e:
print(f"Error importing backend modules: {e}")
sys.exit(1)
Expand All @@ -36,8 +37,8 @@
"answer_model": "gemma-3-27b-it",
"number_of_initial_queries": 2,
"max_research_loops": 1,
"require_planning_confirmation": False
}
"require_planning_confirmation": False,
},
},
{
"name": "02_solid_state_batteries_deep",
Expand All @@ -48,8 +49,8 @@
"answer_model": "gemma-3-27b-it",
"number_of_initial_queries": 4,
"max_research_loops": 3,
"require_planning_confirmation": False
}
"require_planning_confirmation": False,
},
},
{
"name": "03_remote_work_broad",
Expand All @@ -60,8 +61,8 @@
"answer_model": "gemma-3-27b-it",
"number_of_initial_queries": 6,
"max_research_loops": 2,
"require_planning_confirmation": False
}
"require_planning_confirmation": False,
},
},
{
"name": "04_rust_vs_cpp_technical",
Expand All @@ -72,14 +73,15 @@
"answer_model": "gemma-3-27b-it",
"number_of_initial_queries": 3,
"max_research_loops": 2,
"require_planning_confirmation": False
}
}
"require_planning_confirmation": False,
},
},
]

OUTPUT_DIR = REPO_ROOT / "docs" / "sample_reports"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)


async def generate_report(run_config):
name = run_config["name"]
topic = run_config["topic"]
Expand All @@ -89,18 +91,16 @@ async def generate_report(run_config):
print(f"Topic: {topic}")
print(f"Config: {conf_dict}")

inputs = {
"messages": [HumanMessage(content=topic)]
}
inputs = {"messages": [HumanMessage(content=topic)]}

# Configuration wrapper for LangGraph
# We pass the Configuration object fields via 'configurable' dict
runnable_config = {
"configurable": {
"thread_id": f"sample_report_{name}_{int(datetime.now().timestamp())}",
**conf_dict
**conf_dict,
},
"recursion_limit": 100
"recursion_limit": 100,
}

report_content = ""
Expand All @@ -123,15 +123,16 @@ async def generate_report(run_config):
duration = (datetime.now() - start_time).total_seconds()
metadata = {
"duration_seconds": duration,
"total_steps": len(final_state.get("messages", [])), # Proxy for steps
"total_steps": len(final_state.get("messages", [])), # Proxy for steps
"research_loops_performed": conf_dict.get("max_research_loops"),
"model": conf_dict.get("query_generator_model")
"model": conf_dict.get("query_generator_model"),
}

except Exception as e:
print(f"Error generating report for {name}: {e}")
report_content = f"Error generating report: {str(e)}"
import traceback

traceback.print_exc()

# Save Artifacts
Expand All @@ -140,22 +141,27 @@ async def generate_report(run_config):

header = f"""# Sample Report: {topic}
> **Configuration**: {name}
> **Model**: {conf_dict['query_generator_model']}
> **Depth**: {conf_dict['max_research_loops']} Loops
> **Breadth**: {conf_dict['number_of_initial_queries']} Initial Queries
> **Duration**: {metadata.get('duration_seconds', 0):.2f}s
> **Model**: {conf_dict["query_generator_model"]}
> **Depth**: {conf_dict["max_research_loops"]} Loops
> **Breadth**: {conf_dict["number_of_initial_queries"]} Initial Queries
> **Duration**: {metadata.get("duration_seconds", 0):.2f}s

---

"""
with open(md_filename, "w", encoding="utf-8") as f:
f.write(header + report_content)
# SonarCloud: Use an asynchronous file API instead of synchronous open() in this async function.
import aiofiles

async with aiofiles.open(md_filename, "w", encoding="utf-8") as f:
await f.write(header + report_content)

print(f"Saved report to {md_filename}")
return metadata


async def main():
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--index", type=int, help="Index of config to run (0-3)")
args = parser.parse_args()
Expand All @@ -175,5 +181,6 @@ async def main():
print("\nAll runs complete.")
print(json.dumps(results, indent=2))


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading