From 366ec8dee74fe3ad4dfa9a2162e90e0b8e6f928d Mon Sep 17 00:00:00 2001 From: kuzakhmetovartur Date: Mon, 29 Jun 2026 12:13:24 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20solution=20for=20'=D0=9F=D0=BE=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BA=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD:=20=D0=98=D1=81=D1=81=D0=BB=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B1=D1=80=D0=B8=D1=84=20(=D0=BF=D0=BB=D0=B0=D0=BD?= =?UTF-8?q?=20=E2=86=92=20=D1=88=D0=B0=D0=B3=D0=B8=20=E2=86=92=20=D1=81?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=BA=D0=B0)'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 ++ 2 | 1 + 2-3-tavily | 1 + 8-deep-agents-from-scratch | 2 +- README.md | 103 ++++++++++++++++++++++++++------- requirements.txt | 4 +- src/graph.py | 110 ++++++++---------------------------- src/main.py | 46 ++++++++------- src/nodes.py | 83 +++++++++++++++++++++++++++ src/state.py | 11 ++-- structured-output-union-api | 1 + 11 files changed, 232 insertions(+), 135 deletions(-) create mode 100644 .env.example create mode 160000 2 create mode 160000 2-3-tavily create mode 100644 src/nodes.py create mode 160000 structured-output-union-api diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2a80d6b --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# OpenAI API key +OPENAI_API_KEY=your_openai_api_key_here + +# Tavily API key +TAVILY_API_KEY=your_tavily_api_key_here \ No newline at end of file diff --git a/2 b/2 new file mode 160000 index 0000000..7c8d02b --- /dev/null +++ b/2 @@ -0,0 +1 @@ +Subproject commit 7c8d02b756a49b9e8ce2ded5b21561ca4453e095 diff --git a/2-3-tavily b/2-3-tavily new file mode 160000 index 0000000..08e2e01 --- /dev/null +++ b/2-3-tavily @@ -0,0 +1 @@ +Subproject commit 08e2e01b18ae47c81193aa384221daf7c7fdbf89 diff --git a/8-deep-agents-from-scratch b/8-deep-agents-from-scratch index 380e236..865926c 160000 --- a/8-deep-agents-from-scratch +++ b/8-deep-agents-from-scratch @@ -1 +1 @@ -Subproject commit 380e236ecf06e77b69962b9bc9a40925b6063211 +Subproject commit 865926c0017a4f64a7b3f7dada5b7e41d622b8be diff --git a/README.md b/README.md index 0638a42..0a0da4d 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,90 @@ -# Экзамен: Самокорректирующийся агент +# LangGraph Research Brief Agent -Главная -Мои задания -Экзамен: Самокорректирующийся агент -5Д -EN -Экзамен: Самокорректирующийся агент -Зачёт -Версия 2 -Дедлайн сдачи: 31.08.2026 +This project demonstrates how to build a LangGraph agent that generates a short research brief for a given topic. +The agent: -В работе +1. Creates an outline of 4‑5 research steps. +2. For each step, performs a web search (via Tavily) and writes a concise note. +3. Synthesizes all notes into a coherent brief. -Требуется доработка +## Prerequisites -В вашем репозитории не реализовано требуемое LangGraph‑агент и отсутствует зависимость langgraph, необходимая для выполнения задачи. Пожалуйста, добавьте соответствующую реализацию и обновите требования. +- Python 3.10+ +- A **Tavily** API key (free tier available). +- An **OpenAI** API key (or any compatible LLM provider). -Редактирование ответа +## Setup -Заполните ответ и отправьте работу на проверку преподавателю. +```bash +# Clone the repository +git clone https://github.com/your-username/langgraph-research-brief.git +cd langgraph-research-brief -Тип ответа -Текст -Ссылка -Файлы -Ссылка ( \ No newline at end of file +# Create a virtual environment (optional but recommended) +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +Create a `.env` file in the project root based on the example: + +```bash +cp .env.example .env +``` + +Edit `.env` and replace the placeholders with your actual keys: + +``` +OPENAI_API_KEY=sk-... +TAVILY_API_KEY=your_tavily_key +``` + +## Running the Agent + +```bash +python src/main.py +``` + +You should see output similar to: + +``` +=== Outline === +1. Identify the security requirements for MCP integration +2. Review LangChain's authentication mechanisms +3. Evaluate secure communication protocols +4. Test the integration in a sandbox environment +5. Document best practices and compliance checks + +=== Notes === +[Step 1] ... (5‑8 sentence note) +[Step 2] ... (5‑8 sentence note) +... + +=== Final Brief === +... +``` + +## Project Structure + +``` +src/ +├── main.py # Entry point +├── graph.py # LangGraph definition +├── nodes.py # Node implementations +├── state.py # TypedDict for state +├── .env.example # Environment variable template +requirements.txt +README.md +``` + +## Customization + +- **Topic**: Change the `default_topic` variable in `src/main.py` to generate a brief on a different subject. +- **LLM**: Swap `ChatOpenAI` for another provider (e.g., Ollama) by adjusting the imports and initialization in `src/nodes.py`. +- **Search**: Replace `TavilySearchResults` with another search tool if desired. + +## License + +MIT License \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c341710..2d94960 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ langgraph langchain-openai -openai \ No newline at end of file +langchain-tavily +tavily-python +python-dotenv \ No newline at end of file diff --git a/src/graph.py b/src/graph.py index d6d0f0e..1dc7269 100644 --- a/src/graph.py +++ b/src/graph.py @@ -1,92 +1,28 @@ -import json -from typing import Dict, Any -from langgraph.graph import StateGraph, END -from langchain_openai import ChatOpenAI -from .state import PlanningState +from langgraph import StateGraph +from src.state import BriefState +from src.nodes import outline_node, research_step_node, synthesize_node -def planning(state: PlanningState) -> PlanningState: - """LLM node that splits the task into 3‑6 concrete steps.""" - llm = ChatOpenAI(temperature=0) - prompt = ( - f"Task: {state['task']}\n\n" - "Please break this task into 3-6 concrete steps. " - "Return the steps as a numbered list or a JSON array. " - "Do not add any extra text." - ) - response = llm.invoke(prompt) - text = response.content.strip() +def build_graph() -> StateGraph: + graph = StateGraph(BriefState) - # Try to parse JSON first - plan: List[str] | None = None - try: - parsed = json.loads(text) - if isinstance(parsed, list): - plan = [str(item) for item in parsed] - except Exception: - pass + # Add nodes + graph.add_node("outline", outline_node) + graph.add_node("research_step", research_step_node) + graph.add_node("synthesize", synthesize_node) - # Fallback: parse numbered list - if plan is None: - plan = [] - for line in text.splitlines(): - line = line.strip() - if not line: - continue - # Remove leading number if present - if '.' in line: - _, rest = line.split('.', 1) - step = rest.strip() - else: - step = line - plan.append(step) + # Define the condition for looping research steps + def condition(state: BriefState): + if state["step_index"] < len(state["outline"]): + return "research_step" + else: + return "synthesize" - state["plan"] = plan - state["current_step"] = 0 - state["results"] = [] - return state + # Build edges + graph.add_edge("outline", "research_step") + graph.add_conditional_edges("research_step", condition, { + "research_step": "research_step", + "synthesize": "synthesize" + }) + graph.add_edge("synthesize", "__end__") -def execution(state: PlanningState) -> PlanningState: - """Execute one step of the plan.""" - llm = ChatOpenAI(temperature=0) - step = state["plan"][state["current_step"]] - prompt = ( - f"Task: {state['task']}\n\n" - f"You are executing step {state['current_step'] + 1} of the plan.\n\n" - f"Step: {step}\n\n" - "Provide the result of this step." - ) - response = llm.invoke(prompt) - result = response.content.strip() - state["results"].append(result) - state["current_step"] += 1 - return state - -def should_continue(state: PlanningState) -> str: - """Decide whether to loop back to execution or finish.""" - if state["current_step"] < len(state["plan"]): - return "execute" - return "finish" - -def create_graph() -> StateGraph: - graph = StateGraph(PlanningState) - - graph.add_node("planning", planning) - graph.add_node("execution", execution) - graph.add_node("finish", lambda state: state) - - graph.add_conditional_edges( - "planning", - lambda _: "execute", - {"execute": "execution"} - ) - - graph.add_conditional_edges( - "execution", - should_continue, - {"execute": "execution", "finish": "finish"} - ) - - graph.set_entry_point("planning") - graph.set_finish_point("finish") - - return graph \ No newline at end of file + return graph.compile() \ No newline at end of file diff --git a/src/main.py b/src/main.py index 3fcad1c..7beaf86 100644 --- a/src/main.py +++ b/src/main.py @@ -1,34 +1,38 @@ import os -from src.graph import create_graph -from src.state import PlanningState +from dotenv import load_dotenv +from src.graph import build_graph +from src.state import BriefState -def main() -> None: - # Ensure the OpenAI API key is set - if "OPENAI_API_KEY" not in os.environ: - raise RuntimeError("Please set the OPENAI_API_KEY environment variable.") +def main(): + # Load environment variables + load_dotenv() + # Default topic + default_topic = "Как студенту безопасно подключать MCP к LangChain" - task = "Compare Python and JavaScript" - initial_state: PlanningState = { - "task": task, - "plan": None, - "current_step": 0, - "results": [] + # Initial state + initial_state: BriefState = { + "topic": default_topic, + "outline": None, + "step_index": 0, + "notes": [], + "final_brief": None } - graph = create_graph() + # Build and run the graph + graph = build_graph() final_state = graph.invoke(initial_state) - print("\n=== Plan ===") - for i, step in enumerate(final_state["plan"], 1): + # Print results + print("\n=== Outline ===") + for i, step in enumerate(final_state["outline"], 1): print(f"{i}. {step}") - print("\n=== Results ===") - for i, res in enumerate(final_state["results"], 1): - print(f"[Step {i}] {res}") + print("\n=== Notes ===") + for i, note in enumerate(final_state["notes"], 1): + print(f"[Step {i}] {note}\n") - print("\n=== Final Summary ===") - summary = "\n".join(final_state["results"]) - print(summary) + print("\n=== Final Brief ===") + print(final_state["final_brief"]) if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/nodes.py b/src/nodes.py new file mode 100644 index 0000000..48ca2f6 --- /dev/null +++ b/src/nodes.py @@ -0,0 +1,83 @@ +import os +import re +from typing import Dict, Any +from langchain_openai import ChatOpenAI +from langchain_tavily import TavilySearchResults +from src.state import BriefState + +# Load API keys from environment +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") + +# Initialize LLM and Tavily +llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2, openai_api_key=OPENAI_API_KEY) +tavily = TavilySearchResults(tavily_api_key=TAVILY_API_KEY, max_results=3) + +def outline_node(state: BriefState) -> Dict[str, Any]: + """ + Generate a concise outline of 4-5 research steps for the given topic. + """ + topic = state["topic"] + prompt = ( + f"Create a concise outline of 4-5 research steps for the topic: \"{topic}\".\n" + "Return the steps as a numbered list, one step per line." + ) + response = llm.invoke(prompt) + text = response.content if hasattr(response, "content") else str(response) + + # Extract lines that look like numbered steps + steps = re.findall(r"^\s*\d+\.\s*(.+)$", text, re.MULTILINE) + if not steps: + # Fallback: split by newlines + steps = [line.strip() for line in text.splitlines() if line.strip()] + state["outline"] = steps + state["step_index"] = 0 + state["notes"] = [] + return {"outline": steps, "step_index": 0, "notes": []} + +def research_step_node(state: BriefState) -> Dict[str, Any]: + """ + Perform a web search for the current outline step and generate a short note. + """ + outline = state["outline"] + idx = state["step_index"] + if idx >= len(outline): + return {} + + current_step = outline[idx] + # Perform web search + search_results = tavily.invoke({"query": current_step}) + # Prepare a prompt for summarization + prompt = ( + f"Using the following web search results, write a concise note (5-8 sentences) about the topic:\n\n" + f"{search_results}\n\n" + f"Note:\n" + ) + response = llm.invoke(prompt) + note = response.content if hasattr(response, "content") else str(response) + + # Append note and increment step_index + notes = state["notes"] + [note.strip()] + state["notes"] = notes + state["step_index"] = idx + 1 + return {"notes": notes, "step_index": idx + 1} + +def synthesize_node(state: BriefState) -> Dict[str, Any]: + """ + Combine all notes into a coherent brief with headings. + """ + outline = state["outline"] + notes = state["notes"] + combined = "" + for step, note in zip(outline, notes): + combined += f"**{step}**\n\n{note}\n\n" + + prompt = ( + f"Using the following sections, write a concise research brief (1 page) that summarizes the key points.\n\n" + f"{combined}\n\n" + f"Brief:\n" + ) + response = llm.invoke(prompt) + final_brief = response.content if hasattr(response, "content") else str(response) + state["final_brief"] = final_brief.strip() + return {"final_brief": state["final_brief"]} \ No newline at end of file diff --git a/src/state.py b/src/state.py index b518f98..0d92a0e 100644 --- a/src/state.py +++ b/src/state.py @@ -1,7 +1,8 @@ from typing import TypedDict, List, Optional -class PlanningState(TypedDict): - task: str - plan: Optional[List[str]] - current_step: int - results: List[str] \ No newline at end of file +class BriefState(TypedDict): + topic: str + outline: List[str] | None + step_index: int + notes: List[str] + final_brief: str | None \ No newline at end of file diff --git a/structured-output-union-api b/structured-output-union-api new file mode 160000 index 0000000..993512b --- /dev/null +++ b/structured-output-union-api @@ -0,0 +1 @@ +Subproject commit 993512bd88febd5bb29fb97ed91e14d1f39cb092