feat: solution for 'Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily)'
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.env
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
@@ -0,0 +1,64 @@
|
||||
# LangGraph Comparative Review Agent
|
||||
|
||||
This project implements a LangGraph agent that, given three entities (e.g., technologies, products, or approaches), produces a comparative review. The agent:
|
||||
|
||||
1. Generates 3–5 comparison criteria using an LLM.
|
||||
2. Performs a web search for each entity‑criterion pair via Tavily and stores a short note.
|
||||
3. Builds a Markdown table with the findings.
|
||||
4. Produces a verdict recommending which entity suits which use case.
|
||||
|
||||
## Features
|
||||
|
||||
- **LLM powered**: Uses OpenAI’s GPT model to generate criteria and verdicts.
|
||||
- **Web search**: Uses Tavily to fetch up-to-date information for each pair.
|
||||
- **CLI**: Run from the command line with default or custom entities.
|
||||
- **Modular**: Separate files for state, nodes, graph, and CLI.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Create a virtual environment (optional but recommended)
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Windows use `.venv\Scripts\activate`
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Create a .env file with your API keys
|
||||
cp .env.example .env
|
||||
# Edit .env and fill in your keys
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
The script will compare the default entities: **Chroma, FAISS, Qdrant**.
|
||||
You can also provide custom entities:
|
||||
|
||||
```bash
|
||||
python src/main.py --entities "TensorFlow, PyTorch, JAX"
|
||||
```
|
||||
|
||||
The output will display:
|
||||
|
||||
1. Generated comparison criteria.
|
||||
2. The Markdown table of findings.
|
||||
3. The final verdict.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── cli.py # CLI entry point
|
||||
├── graph.py # LangGraph definition
|
||||
├── main.py # Script to run the graph
|
||||
├── nodes.py # Node implementations
|
||||
└── state.py # TypedDict for state
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
@@ -0,0 +1,6 @@
|
||||
langgraph
|
||||
langchain-openai
|
||||
langchain-tavily
|
||||
tavily-python
|
||||
python-dotenv
|
||||
openai
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import argparse
|
||||
from typing import List
|
||||
|
||||
from .state import CompareState
|
||||
from .graph import create_graph
|
||||
|
||||
def parse_entities(arg: str) -> List[str]:
|
||||
return [e.strip() for e in arg.split(",") if e.strip()]
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="LangGraph Comparative Review Agent")
|
||||
parser.add_argument(
|
||||
"--entities",
|
||||
type=str,
|
||||
help="Comma-separated list of three entities to compare. "
|
||||
"If omitted, defaults to Chroma, FAISS, Qdrant.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.entities:
|
||||
entities = parse_entities(args.entities)
|
||||
if len(entities) != 3:
|
||||
raise ValueError("Please provide exactly three entities.")
|
||||
else:
|
||||
entities = ["Chroma", "FAISS", "Qdrant"]
|
||||
|
||||
initial_state: CompareState = {
|
||||
"entities": entities,
|
||||
}
|
||||
|
||||
graph = create_graph()
|
||||
final_state = graph.invoke(initial_state)
|
||||
|
||||
print("\n=== Comparison Criteria ===")
|
||||
for idx, crit in enumerate(final_state["criteria"], 1):
|
||||
print(f"{idx}. {crit}")
|
||||
|
||||
print("\n=== Findings Table ===")
|
||||
print(final_state["final_table"])
|
||||
|
||||
print("\n=== Verdict ===")
|
||||
print(final_state["verdict"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
from langgraph.graph import StateGraph
|
||||
from .state import CompareState
|
||||
from .nodes import plan_criteria, research_entity, build_table, verdict
|
||||
|
||||
def create_graph() -> StateGraph:
|
||||
graph = StateGraph(CompareState)
|
||||
|
||||
# Add nodes
|
||||
graph.add_node("plan_criteria", plan_criteria)
|
||||
graph.add_node("research_entity", research_entity)
|
||||
graph.add_node("build_table", build_table)
|
||||
graph.add_node("verdict", verdict)
|
||||
|
||||
# Define edges
|
||||
graph.set_entry_point("plan_criteria")
|
||||
graph.add_edge("plan_criteria", "research_entity")
|
||||
graph.add_edge("research_entity", "build_table")
|
||||
graph.add_edge("build_table", "verdict")
|
||||
graph.add_edge("verdict", END)
|
||||
|
||||
return graph
|
||||
@@ -0,0 +1,4 @@
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import os
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from langgraph.graph import StateGraph, END
|
||||
from langchain_openai import ChatOpenAI
|
||||
from tavily import TavilyClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from .state import CompareState
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Initialize LLM and Tavily client
|
||||
llm = ChatOpenAI(
|
||||
temperature=0.2,
|
||||
model="gpt-4o-mini",
|
||||
openai_api_key=os.getenv("OPENAI_API_KEY"),
|
||||
)
|
||||
|
||||
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
|
||||
|
||||
def plan_criteria(state: CompareState) -> CompareState:
|
||||
"""
|
||||
Generate 3–5 comparison criteria for the given entities.
|
||||
"""
|
||||
entities = state.get("entities", [])
|
||||
if not entities:
|
||||
raise ValueError("No entities provided for criteria planning.")
|
||||
|
||||
prompt = (
|
||||
f"Given the following entities: {', '.join(entities)}.\n"
|
||||
"Suggest 3 to 5 key criteria to compare them. "
|
||||
"Return the criteria as a numbered list, one per line."
|
||||
)
|
||||
response = llm.invoke(prompt)
|
||||
criteria_text = response.content.strip()
|
||||
# Parse numbered list
|
||||
criteria = []
|
||||
for line in criteria_text.splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
# Remove leading numbers if present
|
||||
if line[0].isdigit() and (len(line) > 1 and line[1] in ". "):
|
||||
line = line[2:].strip()
|
||||
criteria.append(line)
|
||||
state["criteria"] = criteria
|
||||
return state
|
||||
|
||||
def research_entity(state: CompareState) -> CompareState:
|
||||
"""
|
||||
For each entity–criterion pair, perform a Tavily web search
|
||||
and store a short note in findings.
|
||||
"""
|
||||
entities = state.get("entities", [])
|
||||
criteria = state.get("criteria", [])
|
||||
findings: Dict[str, List[str]] = {entity: [] for entity in entities}
|
||||
|
||||
for entity in entities:
|
||||
for criterion in criteria:
|
||||
query = f"{entity} {criterion}"
|
||||
try:
|
||||
result = tavily.search(query=query, max_results=1)
|
||||
if result and result["results"]:
|
||||
snippet = result["results"][0]["content"][:200]
|
||||
else:
|
||||
snippet = "No relevant information found."
|
||||
except Exception as e:
|
||||
snippet = f"Error during search: {e}"
|
||||
findings[entity].append(snippet)
|
||||
|
||||
state["findings"] = findings
|
||||
return state
|
||||
|
||||
def build_table(state: CompareState) -> CompareState:
|
||||
"""
|
||||
Build a Markdown table from findings.
|
||||
Rows: criteria, Columns: entities.
|
||||
"""
|
||||
entities = state.get("entities", [])
|
||||
criteria = state.get("criteria", [])
|
||||
findings = state.get("findings", {})
|
||||
|
||||
header = "| Criterion | " + " | ".join(entities) + " |\n"
|
||||
separator = "|---" * (len(entities) + 1) + "|\n"
|
||||
|
||||
rows = ""
|
||||
for idx, criterion in enumerate(criteria):
|
||||
row = f"| {criterion} | "
|
||||
for entity in entities:
|
||||
notes = findings.get(entity, [])
|
||||
note = notes[idx] if idx < len(notes) else ""
|
||||
# Escape pipe characters
|
||||
note = note.replace("|", "\\|")
|
||||
row += f"{note} | "
|
||||
rows += row + "\n"
|
||||
|
||||
table = header + separator + rows
|
||||
state["final_table"] = table
|
||||
return state
|
||||
|
||||
def verdict(state: CompareState) -> CompareState:
|
||||
"""
|
||||
Generate a verdict recommendation based on the table and criteria.
|
||||
"""
|
||||
table = state.get("final_table", "")
|
||||
criteria = state.get("criteria", [])
|
||||
entities = state.get("entities", [])
|
||||
|
||||
prompt = (
|
||||
f"Here is a comparative table of the following entities: {', '.join(entities)}.\n\n"
|
||||
f"{table}\n\n"
|
||||
f"Based on the criteria: {', '.join(criteria)}.\n"
|
||||
"Provide a concise recommendation (2–4 sentences) indicating which entity is best suited for which use case."
|
||||
)
|
||||
response = llm.invoke(prompt)
|
||||
state["verdict"] = response.content.strip()
|
||||
return state
|
||||
@@ -0,0 +1,8 @@
|
||||
from typing import TypedDict, List, Dict, Optional
|
||||
|
||||
class CompareState(TypedDict, total=False):
|
||||
entities: List[str] # 3 names to compare
|
||||
criteria: List[str] # 3–5 comparison criteria
|
||||
findings: Dict[str, List[str]] # entity -> list of notes per criterion
|
||||
final_table: Optional[str]
|
||||
verdict: Optional[str]
|
||||
Reference in New Issue
Block a user