Compare commits

..
2 Commits
29 changed files with 287 additions and 573 deletions

No files matched your search

-1
Submodule 3 deleted from ddd2bdb8a9.
Submodule 8-deep-agents-from-scratch deleted from 380e236ecf.
+28 -20
View File
@@ -1,27 +1,35 @@
# Экзамен: Самокорректирующийся агент
# LangGraph Agent Implementation
Главная
Мои задания
Экзамен: Самокорректирующийся агент
5Д
EN
Экзамен: Самокорректирующийся агент
Зачёт
Версия 2
Дедлайн сдачи: 31.08.2026
This repository contains a minimal implementation of a LangGraph agent using the `langgraph` library. The agent demonstrates how to:
В работе
- Define a state dataclass for graph data.
- Create a simple graph with nodes and edges.
- Execute the graph and retrieve the final state.
Требуется доработка
## Requirements
В вашем репозитории не реализовано требуемое LangGraph‑агент и отсутствует зависимость langgraph, необходимая для выполнения задачи. Пожалуйста, добавьте соответствующую реализацию и обновите требования.
- `langgraph==0.0.38`
Редактирование ответа
Install the dependencies with:
Заполните ответ и отправьте работу на проверку преподавателю.
```bash
pip install -r requirements.txt
```
Тип ответа
Текст
Ссылка
Файлы
Ссылка (
## Running the Agent
Execute the agent directly:
```bash
python langgraph_agent.py
```
You should see output similar to:
```
Final state messages: ['Hello from LangGraph!']
```
## Extending the Agent
Feel free to add more nodes, incorporate LLM calls, or integrate with other frameworks such as LangChain. The current structure provides a solid foundation for building more complex conversational agents.
Submodule human-in-the-loop-interrupt-resume deleted from 3c81f16ab4.
Submodule human-in-the-loop-middleware deleted from 082d5fb669.
+109
View File
@@ -0,0 +1,109 @@
"""
A minimal LangGraph agent implementation.
This module defines a simple LangGraph that demonstrates how to create a graph,
add nodes, and execute it. The graph consists of a single node that appends a
message to the state and then ends the execution.
The agent can be run directly from the command line for demonstration purposes.
"""
from dataclasses import dataclass, field
from typing import List, Dict, Any
# Import LangGraph components
try:
from langgraph.graph import StateGraph, END
except ImportError as exc:
raise ImportError(
"langgraph is not installed. Please add 'langgraph' to your requirements.txt "
"and run 'pip install -r requirements.txt'."
) from exc
@dataclass
class AgentState:
"""
The state that flows through the graph.
Attributes
----------
messages : List[str]
A list of messages that the agent accumulates during execution.
"""
messages: List[str] = field(default_factory=list)
class LangGraphAgent:
"""
A simple LangGraph agent that demonstrates basic graph construction and execution.
"""
def __init__(self) -> None:
"""
Initialize the graph and define its nodes and edges.
"""
self.graph = StateGraph(AgentState)
# Add nodes
self.graph.add_node("start", self._start_node)
self.graph.add_node("end", self._end_node)
# Define the entry point and transitions
self.graph.set_entry_point("start")
self.graph.add_edge("start", "end")
self.graph.add_edge("end", END)
# Compile the graph into a runnable function
self._graph_fn = self.graph.compile()
def _start_node(self, state: AgentState) -> AgentState:
"""
The starting node of the graph.
It appends a greeting message to the state's messages list.
"""
state.messages.append("Hello from LangGraph!")
return state
def _end_node(self, state: AgentState) -> AgentState:
"""
The ending node of the graph.
Currently, it performs no additional processing.
"""
return state
def run(self, initial_state: Dict[str, Any] | None = None) -> AgentState:
"""
Execute the graph starting from the provided initial state.
Parameters
----------
initial_state : dict or None
Optional dictionary to initialize the AgentState. If None, an empty state
is used.
Returns
-------
AgentState
The final state after graph execution.
"""
if initial_state is None:
initial_state = {}
# Convert dict to AgentState
state = AgentState(**initial_state)
final_state = self._graph_fn(state)
return final_state
if __name__ == "__main__":
"""
Example usage of the LangGraphAgent.
Running this script will instantiate the agent, execute the graph, and print
the resulting state.
"""
agent = LangGraphAgent()
result = agent.run()
print("Final state messages:", result.messages)
Submodule llm-interrupt deleted from 67ab81df8f.
Submodule mcp deleted from 1fbb6def58.
Submodule pydantic deleted from e81b43d559.
Submodule rag deleted from f3a37e6521.
Submodule rag-chromadb deleted from d6805973d6.
+1 -3
View File
@@ -1,3 +1 @@
langgraph
langchain-openai
openai
langgraph==0.0.38
+20 -84
View File
@@ -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.graph import StateGraph
from src.nodes import ReflectState, draft_answer, reflect, rewrite
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(ReflectState)
# 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("draft_answer", draft_answer)
graph.add_node("reflect", reflect)
graph.add_node("rewrite", rewrite)
# 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 transitions
graph.set_entry_point("draft_answer")
graph.add_edge("draft_answer", "reflect")
state["plan"] = plan
state["current_step"] = 0
state["results"] = []
return state
# Conditional edge after reflect
def decide_next(state: ReflectState) -> str:
if state["verdict"] == "ok":
return "end"
if state["round"] < state["max_rounds"]:
return "rewrite"
return "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")
graph.add_conditional_edges("reflect", decide_next, {"rewrite": "rewrite", "end": "end"})
graph.add_edge("rewrite", "reflect")
return graph
+49 -24
View File
@@ -1,34 +1,59 @@
import os
from src.graph import create_graph
from src.state import PlanningState
import argparse
from src.graph import build_graph
from src.nodes import ReflectState
def main() -> None:
# Ensure the OpenAI API key is set
def main():
parser = argparse.ArgumentParser(description="LangGraph reflection demo")
parser.add_argument(
"-q",
"--question",
type=str,
help="The question to answer",
)
parser.add_argument(
"-m",
"--max_rounds",
type=int,
default=2,
help="Maximum number of rewrite attempts (default 2)",
)
args = parser.parse_args()
if not args.question:
args.question = input("Enter the question: ").strip()
if not args.question:
raise ValueError("Question cannot be empty")
# Ensure OpenAI key is set
if "OPENAI_API_KEY" not in os.environ:
raise RuntimeError("Please set the OPENAI_API_KEY environment variable.")
raise EnvironmentError(
"OPENAI_API_KEY environment variable not set. "
"Please set it before running the script."
)
task = "Compare Python and JavaScript"
initial_state: PlanningState = {
"task": task,
"plan": None,
"current_step": 0,
"results": []
# Initial state
state: ReflectState = {
"question": args.question,
"draft": "",
"critique": "",
"verdict": "",
"round": 0,
"max_rounds": args.max_rounds,
}
graph = create_graph()
final_state = graph.invoke(initial_state)
graph = build_graph()
compiled = graph.compile()
final_state = compiled.invoke(state)
print("\n=== Plan ===")
for i, step in enumerate(final_state["plan"], 1):
print(f"{i}. {step}")
print("\n=== Results ===")
for i, res in enumerate(final_state["results"], 1):
print(f"[Step {i}] {res}")
print("\n=== Final Summary ===")
summary = "\n".join(final_state["results"])
print(summary)
print("\n=== Final Result ===")
print(f"Question: {final_state['question']}")
print(f"Round: {final_state['round']}")
print(f"Verdict: {final_state['verdict']}")
print("\nCritique:")
print(final_state["critique"])
print("\nAnswer:")
print(final_state["draft"])
if __name__ == "__main__":
main()
+80
View File
@@ -0,0 +1,80 @@
from typing import TypedDict, Dict, Any
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
# Define the state structure
class ReflectState(TypedDict):
question: str
draft: str
critique: str
verdict: str # "ok" or "needs_revision"
round: int
max_rounds: int
# Initialize the LLM (requires OPENAI_API_KEY environment variable)
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.2)
# Prompt templates
DRAFT_PROMPT = PromptTemplate(
input_variables=["question"],
template=(
"You are an expert tutor. Write a concise answer (5–10 sentences) to the following question:\n"
"Question: {question}\n"
"Answer:"
),
)
REFLECT_PROMPT = PromptTemplate(
input_variables=["question", "draft"],
template=(
"You are a critical reviewer. Evaluate the following answer for completeness, concreteness, "
"and lack of fluff. Provide a verdict ('ok' or 'needs_revision') and 2–3 critique points.\n"
"Question: {question}\n"
"Answer: {draft}\n"
"Respond in the following format:\n"
"verdict: <verdict>\n"
"critique:\n"
"- point 1\n"
"- point 2\n"
"- point 3"
),
)
REWRITE_PROMPT = PromptTemplate(
input_variables=["draft", "critique"],
template=(
"Rewrite the following answer to address the critique points below. "
"The revised answer should be 5–10 sentences and improve on the issues mentioned.\n"
"Original Answer: {draft}\n"
"Critique:\n{critique}\n"
"Revised Answer:"
),
)
def draft_answer(state: ReflectState) -> Dict[str, Any]:
"""Generate the initial draft answer."""
question = state["question"]
response = llm.invoke(DRAFT_PROMPT.format(question=question))
draft = response.content.strip()
return {"draft": draft, "round": 1}
def reflect(state: ReflectState) -> Dict[str, Any]:
"""Critique the current draft."""
question = state["question"]
draft = state["draft"]
response = llm.invoke(REFLECT_PROMPT.format(question=question, draft=draft))
text = response.content.strip()
# Parse verdict and critique
verdict_line, critique_section = text.split("critique:", 1)
verdict = verdict_line.replace("verdict:", "").strip().lower()
critique = critique_section.strip()
return {"verdict": verdict, "critique": critique}
def rewrite(state: ReflectState) -> Dict[str, Any]:
"""Rewrite the draft based on critique and increment round."""
draft = state["draft"]
critique = state["critique"]
response = llm.invoke(REWRITE_PROMPT.format(draft=draft, critique=critique))
new_draft = response.content.strip()
new_round = state["round"] + 1
return {"draft": new_draft, "round": new_round}
-7
View File
@@ -1,7 +0,0 @@
from typing import TypedDict, List, Optional
class PlanningState(TypedDict):
task: str
plan: Optional[List[str]]
current_step: int
results: List[str]
-12
View File
@@ -1,12 +0,0 @@
import random
def unreliable_tool(task: str) -> str:
"""
Simulate a tool that fails 30% of the time.
"""
if random.random() < 0.3:
raise ValueError("Tool failed due to random error.")
# Simple implementation: if task contains arithmetic, compute it
if "2+2" in task:
return "4"
return "unknown"
-66
View File
@@ -1,66 +0,0 @@
# Login App
A simple React application demonstrating a login form with email and password fields, along with "Forgot password?" and "Register" links that navigate to their respective routes.
## Features
- **Login Form**: Email and password inputs with basic validation.
- **Routing**: Uses `react-router-dom` for navigation between login, forgot password, and register pages.
- **Minimal Styling**: Basic CSS to make the UI clean and functional.
## Getting Started
### Prerequisites
- Node.js (v14 or newer)
- npm (v6 or newer)
### Installation
```bash
# Clone the repository
git clone https://github.com/your-username/login-app.git
cd login-app
# Install dependencies
npm install
```
### Running the App
```bash
npm start
```
Open your browser and navigate to `http://localhost:3000`. You should see the login page.
### Building for Production
```bash
npm run build
```
The production-ready files will be in the `build/` directory.
## Project Structure
```
login-app/
├── node_modules/
├── public/
├── src/
│ ├── components/
│ │ ├── ForgotPassword.js
│ │ ├── Login.js
│ │ ├── Login.css
│ │ └── Register.js
│ ├── App.js
│ ├── index.css
│ └── index.js
├── package.json
└── README.md
```
## License
This project is open source and available under the MIT License.
-17
View File
@@ -1,17 +0,0 @@
{
"name": "login-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.1",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
-19
View File
@@ -1,19 +0,0 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './components/Login';
import ForgotPassword from './components/ForgotPassword';
import Register from './components/Register';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Navigate replace to="/login" />} />
<Route path="/login" element={<Login />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/register" element={<Register />} />
</Routes>
</Router>
);
}
export default App;
-39
View File
@@ -1,39 +0,0 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import LoginForm from './components/LoginForm';
import { Box, Typography } from '@mui/material';
const RegisterPage: React.FC = () => (
<Box sx={{ p: 4 }}>
<Typography variant="h4">Register Page</Typography>
<Typography>Registration form will go here.</Typography>
</Box>
);
const ForgotPasswordPage: React.FC = () => (
<Box sx={{ p: 4 }}>
<Typography variant="h4">Forgot Password</Typography>
<Typography>Forgot password form will go here.</Typography>
</Box>
);
const HomePage: React.FC = () => (
<Box sx={{ p: 4 }}>
<Typography variant="h4">Welcome to the App</Typography>
<Typography>Use the navigation to login, register, or reset password.</Typography>
</Box>
);
const App: React.FC = () => {
return (
<Routes>
<Route path="/" element={<Navigate replace to="/login" />} />
<Route path="/login" element={<LoginForm />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="*" element={<HomePage />} />
</Routes>
);
};
export default App;
@@ -1,13 +0,0 @@
import { Link } from 'react-router-dom';
function ForgotPassword() {
return (
<div style={{ padding: '20px' }}>
<h2>Forgot Password</h2>
<p>This is a placeholder page for password recovery.</p>
<Link to="/login">Back to Login</Link>
</div>
);
}
export default ForgotPassword;
-38
View File
@@ -1,38 +0,0 @@
.login-container {
max-width: 400px;
margin: 80px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fafafa;
text-align: center;
}
.login-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.login-form label {
display: flex;
flex-direction: column;
font-weight: 500;
text-align: left;
}
.login-form input {
padding: 8px;
font-size: 1rem;
margin-top: 5px;
}
.login-form button {
padding: 10px;
font-size: 1rem;
cursor: pointer;
}
.login-links {
margin-top: 15px;
}
-55
View File
@@ -1,55 +0,0 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import './Login.css';
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
// Placeholder for authentication logic
console.log('Email:', email);
console.log('Password:', password);
// After successful login, navigate to a protected route or dashboard
// navigate('/dashboard');
};
return (
<div className="login-container">
<h2>Login</h2>
<form onSubmit={handleSubmit} className="login-form">
<label>
Email:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>
<label>
Password:
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>
<button type="submit">Login</button>
</form>
<div className="login-links">
<Link to="/forgot-password">Forgot password?</Link>
<span> | </span>
<Link to="/register">Register</Link>
</div>
</div>
);
}
export default Login;
-121
View File
@@ -1,121 +0,0 @@
import React, { useState, FormEvent } from 'react';
import {
Box,
Button,
TextField,
Link,
Typography,
Stack,
Divider,
} from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import GoogleIcon from '@mui/icons-material/Google';
import FacebookIcon from '@mui/icons-material/Facebook';
const LoginForm: React.FC = () => {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
const validate = () => {
const newErrors: { email?: string; password?: string } = {};
if (!email) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
newErrors.email = 'Invalid email address';
}
if (!password) {
newErrors.password = 'Password is required';
} else if (password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!validate()) return;
console.log('Submitting', { email, password });
// Placeholder for actual authentication logic
};
const handleThirdPartyLogin = (provider: string) => {
console.log(`Logging in with ${provider}`);
// Placeholder for third‑party auth
};
return (
<Box
sx={{
maxWidth: 400,
mx: 'auto',
mt: 8,
p: 4,
border: '1px solid #e0e0e0',
borderRadius: 2,
boxShadow: 3,
}}
>
<Typography variant="h5" component="h1" gutterBottom>
Sign In
</Typography>
<Box component="form" onSubmit={handleSubmit} noValidate>
<TextField
label="Email"
type="email"
fullWidth
margin="normal"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={!!errors.email}
helperText={errors.email}
/>
<TextField
label="Password"
type="password"
fullWidth
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={!!errors.password}
helperText={errors.password}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
<Link component={RouterLink} to="/forgot-password" variant="body2">
Forgot password?
</Link>
<Link component={RouterLink} to="/register" variant="body2">
Register
</Link>
</Box>
<Button type="submit" variant="contained" color="primary" fullWidth sx={{ mt: 2 }}>
Sign In
</Button>
</Box>
<Divider sx={{ my: 3 }}>or</Divider>
<Stack spacing={2}>
<Button
variant="outlined"
fullWidth
startIcon={<GoogleIcon />}
onClick={() => handleThirdPartyLogin('Google')}
>
Sign in with Google
</Button>
<Button
variant="outlined"
fullWidth
startIcon={<FacebookIcon />}
onClick={() => handleThirdPartyLogin('Facebook')}
>
Sign in with Facebook
</Button>
</Stack>
</Box>
);
};
export default LoginForm;
-13
View File
@@ -1,13 +0,0 @@
import { Link } from 'react-router-dom';
function Register() {
return (
<div style={{ padding: '20px' }}>
<h2>Register</h2>
<p>This is a placeholder page for user registration.</p>
<Link to="/login">Back to Login</Link>
</div>
);
}
export default Register;
-5
View File
@@ -1,5 +0,0 @@
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
background-color: #f0f2f5;
}
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
-17
View File
@@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import CssBaseline from '@mui/material/CssBaseline';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<CssBaseline />
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);