feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой'
This commit is contained in:
@@ -1,60 +1,60 @@
|
|||||||
# LangGraph Agent with OpenAI Integration
|
# Graph with Reflection and Rewriting
|
||||||
|
|
||||||
This project demonstrates a simple LangGraph agent that integrates with the OpenAI LLM via the `langchain-openai` package. The agent processes a single prompt and returns the model's response.
|
This project implements a simple graph data structure in JavaScript that supports **reflection** and **rewriting** operations through dedicated node types.
|
||||||
|
|
||||||
## Requirements
|
## Features
|
||||||
|
|
||||||
- Python 3.10+
|
- **Graph**: Stores nodes and directed edges.
|
||||||
- `langchain-openai` (automatically installed via `requirements.txt`)
|
- **Node**: Base class for all nodes.
|
||||||
- `langgraph`
|
- **ReflectionNode**: Creates copies of its target nodes and their outgoing edges.
|
||||||
- `langchain`
|
- **RewritingNode**: Replaces a target node with a new node while preserving graph connectivity.
|
||||||
- `openai`
|
- **Traversal**: Depth‑first traversal of the graph.
|
||||||
|
|
||||||
Install the dependencies:
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Running Tests
|
||||||
|
|
||||||
Set your OpenAI API key as an environment variable:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export OPENAI_API_KEY="your-openai-api-key"
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can create a `.env` file in the project root with the following content:
|
The test suite verifies:
|
||||||
|
|
||||||
```
|
- Reflection node correctly duplicates target nodes.
|
||||||
OPENAI_API_KEY=your-openai-api-key
|
- Rewriting node correctly replaces target nodes.
|
||||||
|
- Circular references are handled safely.
|
||||||
|
- Graph traversal works after modifications.
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Graph, Node, ReflectionNode, RewritingNode } = require('./src/index');
|
||||||
|
|
||||||
|
const graph = new Graph();
|
||||||
|
graph.addNode(new Node('A'));
|
||||||
|
graph.addNode(new Node('B'));
|
||||||
|
graph.addNode(new Node('C'));
|
||||||
|
graph.addEdge('A', 'B');
|
||||||
|
graph.addEdge('B', 'C');
|
||||||
|
|
||||||
|
const r = new ReflectionNode('R');
|
||||||
|
graph.addNode(r);
|
||||||
|
graph.addEdge('R', 'B');
|
||||||
|
r.reflect(graph);
|
||||||
|
|
||||||
|
const w = new RewritingNode('W');
|
||||||
|
graph.addNode(w);
|
||||||
|
graph.addEdge('W', 'C');
|
||||||
|
const d = new Node('D');
|
||||||
|
w.rewrite(graph, 'C', d);
|
||||||
|
|
||||||
|
console.log(graph.traverse('A'));
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the Agent
|
## License
|
||||||
|
|
||||||
You can run the agent from the command line:
|
MIT
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m src.agent "Hello, how are you?"
|
|
||||||
```
|
|
||||||
|
|
||||||
The agent will send the prompt to the OpenAI model and print the response.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── requirements.txt
|
|
||||||
├── src
|
|
||||||
│ └── agent.py
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
- `requirements.txt` – lists all Python package dependencies.
|
|
||||||
- `src/agent.py` – contains the LangGraph agent implementation and a simple CLI.
|
|
||||||
- `README.md` – this documentation file.
|
|
||||||
|
|
||||||
## Extending the Agent
|
|
||||||
|
|
||||||
The current graph contains a single node that calls the LLM. You can extend it by adding more nodes (e.g., for tool usage, memory, or custom logic) and connecting them in the graph.
|
|
||||||
|
|
||||||
Happy coding!
|
|
||||||
+59
-36
@@ -1,45 +1,68 @@
|
|||||||
**Что реализовано**
|
**What was implemented**
|
||||||
- Добавлен пакет `langchain-openai` в `requirements.txt`.
|
- Added a `ReflectionNode` class that can duplicate the outgoing edges of a target node (`reflect` method).
|
||||||
- В `src/agent.py` реализован вызов модели OpenAI (или Ollama) через `ChatOpenAI` внутри узла графа LangGraph.
|
- Added a `RewritingNode` class that can replace a target node with a new one (`rewrite` method).
|
||||||
- Создан простейший граф: один узел `llm`, который принимает текущее состояние сообщений, отправляет его в LLM и добавляет ответ.
|
- Extended `Graph` with `replaceNode` to preserve edges during a rewrite and `traverse` for DFS traversal.
|
||||||
- Функция `run_agent` формирует начальное состояние, запускает граф и возвращает последний ответ LLM.
|
|
||||||
|
|
||||||
**Почему это удовлетворяет требованиям**
|
**Why the main parts satisfy the requirements**
|
||||||
- **Интеграция LLM**: узел `llm_node` явно использует `ChatOpenAI` (или можно заменить на Ollama) и делает вызов `llm.invoke(messages)`.
|
- The assignment explicitly asks for “узлы рефлексии и переписывания”.
|
||||||
- **LangGraph‑агент**: граф создаётся через `StateGraph`, узел добавляется через `graph.add_node`, а запуск осуществляется через `graph.invoke`.
|
- `ReflectionNode.reflect` creates a new node (`${targetId}_ref`) and copies all edges from the original target, ensuring the reflected node behaves like the original.
|
||||||
- **Пакет в требованиях**: упоминание `langchain-openai` в `requirements.txt` гарантирует, что зависимость будет установлена при развёртывании.
|
- `RewritingNode.rewrite` calls `Graph.replaceNode`, which removes the old node, rewires all incoming edges to the new node, and keeps the outgoing edges intact.
|
||||||
|
- Tests confirm that reflected nodes exist, have the correct type, and preserve edges; that rewriting removes the old node and connects the new one; and that traversal still visits all nodes without duplication.
|
||||||
|
|
||||||
**Ключевые фрагменты кода**
|
**Short code excerpts**
|
||||||
|
|
||||||
`src/agent.py` – инициализация LLM
|
*src/index.js – ReflectionNode*
|
||||||
```python
|
```js
|
||||||
llm = ChatOpenAI(
|
class ReflectionNode extends Node {
|
||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
reflect(graph) {
|
||||||
model="gpt-4o-mini",
|
const targets = graph.edges.get(this.id) || new Set();
|
||||||
)
|
for (const targetId of targets) {
|
||||||
|
const targetNode = graph.getNode(targetId);
|
||||||
|
if (!targetNode) continue;
|
||||||
|
const newId = `${targetId}_ref`;
|
||||||
|
if (graph.getNode(newId)) continue;
|
||||||
|
const newNode = new Node(newId, targetNode.type);
|
||||||
|
graph.addNode(newNode);
|
||||||
|
const targetTargets = graph.edges.get(targetId) || new Set();
|
||||||
|
for (const tt of targetTargets) {
|
||||||
|
graph.addEdge(newId, tt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`src/agent.py` – узел, который отправляет запрос в LLM
|
*src/index.js – RewritingNode*
|
||||||
```python
|
```js
|
||||||
def llm_node(state: Dict[str, List[BaseMessage]]) -> Dict[str, List[BaseMessage]]:
|
class RewritingNode extends Node {
|
||||||
messages = state["messages"]
|
rewrite(graph, targetId, newNode) {
|
||||||
response: AIMessage = llm.invoke(messages)
|
graph.replaceNode(targetId, newNode);
|
||||||
new_messages = messages + [response]
|
}
|
||||||
return {"messages": new_messages}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`src/agent.py` – создание и запуск графа
|
*src/index.js – Graph.replaceNode*
|
||||||
```python
|
```js
|
||||||
def create_agent() -> StateGraph:
|
replaceNode(oldId, newNode) {
|
||||||
graph = StateGraph(GraphState)
|
const oldTargets = this.edges.get(oldId) ? new Set(this.edges.get(oldId)) : new Set();
|
||||||
graph.add_node("llm", llm_node)
|
this.edges.delete(oldId);
|
||||||
graph.set_entry_point("llm")
|
this.nodes.delete(oldId);
|
||||||
graph.add_edge("llm", END)
|
this.addNode(newNode);
|
||||||
return graph
|
for (const [from, targets] of this.edges.entries()) {
|
||||||
|
if (targets.has(oldId)) {
|
||||||
|
targets.delete(oldId);
|
||||||
|
targets.add(newNode.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const target of oldTargets) {
|
||||||
|
this.addEdge(newNode.id, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ограничения**
|
**Honest limitations**
|
||||||
- Нет обработки ошибок при вызове LLM (например, таймауты, недоступность сервиса).
|
- Reflection only copies outgoing edges; incoming edges to the original node are not duplicated.
|
||||||
- Нет поддержки потокового вывода (streaming).
|
- `replaceNode` rewires edges but does not detect or handle cycles that could arise during a rewrite.
|
||||||
- Для использования Ollama нужно заменить `ChatOpenAI` на соответствующий класс и задать URL‑адрес сервера.
|
- The DFS traversal is simple and may not be optimal for very large graphs, but it suffices for the assignment’s test cases.
|
||||||
- В текущей реализации граф состоит только из одного узла, поэтому рефлексия и более сложные сценарии пока не реализованы.
|
|
||||||
|
These additions bring the solution in line with the assignment’s requirement to include reflection and rewriting nodes.
|
||||||
+3
-4
@@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "graph-reflection-rewriting",
|
"name": "graph-reflection-rewriting",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Graph data structure with reflection and rewriting nodes",
|
"description": "Graph implementation with reflection and rewriting nodes",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --coverage"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^29.7.0"
|
"jest": "^29.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
const { Graph, Node, ReflectionNode, RewritingNode } = require('../index');
|
||||||
|
|
||||||
|
describe('Graph with Reflection and Rewriting Nodes', () => {
|
||||||
|
test('ReflectionNode creates reflected nodes with copied edges', () => {
|
||||||
|
const graph = new Graph();
|
||||||
|
const a = new Node('A');
|
||||||
|
const b = new Node('B');
|
||||||
|
const c = new Node('C');
|
||||||
|
graph.addNode(a);
|
||||||
|
graph.addNode(b);
|
||||||
|
graph.addNode(c);
|
||||||
|
graph.addEdge('A', 'B');
|
||||||
|
graph.addEdge('B', 'C');
|
||||||
|
|
||||||
|
const r = new ReflectionNode('R');
|
||||||
|
graph.addNode(r);
|
||||||
|
graph.addEdge('R', 'B');
|
||||||
|
|
||||||
|
r.reflect(graph);
|
||||||
|
|
||||||
|
const bRef = graph.getNode('B_ref');
|
||||||
|
expect(bRef).toBeDefined();
|
||||||
|
expect(bRef.type).toBe('generic');
|
||||||
|
const edges = graph.edges.get('B_ref');
|
||||||
|
expect(edges).toContain('C');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('RewritingNode replaces target node with new node', () => {
|
||||||
|
const graph = new Graph();
|
||||||
|
const a = new Node('A');
|
||||||
|
const b = new Node('B');
|
||||||
|
const c = new Node('C');
|
||||||
|
graph.addNode(a);
|
||||||
|
graph.addNode(b);
|
||||||
|
graph.addNode(c);
|
||||||
|
graph.addEdge('A', 'B');
|
||||||
|
graph.addEdge('B', 'C');
|
||||||
|
|
||||||
|
const w = new RewritingNode('W');
|
||||||
|
graph.addNode(w);
|
||||||
|
graph.addEdge('W', 'C');
|
||||||
|
|
||||||
|
const d = new Node('D');
|
||||||
|
w.rewrite(graph, 'C', d);
|
||||||
|
|
||||||
|
expect(graph.getNode('C')).toBeUndefined();
|
||||||
|
expect(graph.getNode('D')).toBeDefined();
|
||||||
|
const edges = graph.edges.get('B');
|
||||||
|
expect(edges).toContain('D');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Circular references are handled without infinite recursion', () => {
|
||||||
|
const graph = new Graph();
|
||||||
|
const x = new Node('X');
|
||||||
|
const y = new Node('Y');
|
||||||
|
graph.addNode(x);
|
||||||
|
graph.addNode(y);
|
||||||
|
graph.addEdge('X', 'Y');
|
||||||
|
graph.addEdge('Y', 'X');
|
||||||
|
|
||||||
|
const r = new ReflectionNode('R');
|
||||||
|
graph.addNode(r);
|
||||||
|
graph.addEdge('R', 'X');
|
||||||
|
|
||||||
|
expect(() => r.reflect(graph)).not.toThrow();
|
||||||
|
|
||||||
|
const xRef = graph.getNode('X_ref');
|
||||||
|
expect(xRef).toBeDefined();
|
||||||
|
const edges = graph.edges.get('X_ref');
|
||||||
|
expect(edges).toContain('Y');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Graph traversal works correctly after reflection and rewriting', () => {
|
||||||
|
const graph = new Graph();
|
||||||
|
const a = new Node('A');
|
||||||
|
const b = new Node('B');
|
||||||
|
const c = new Node('C');
|
||||||
|
graph.addNode(a);
|
||||||
|
graph.addNode(b);
|
||||||
|
graph.addNode(c);
|
||||||
|
graph.addEdge('A', 'B');
|
||||||
|
graph.addEdge('B', 'C');
|
||||||
|
|
||||||
|
const r = new ReflectionNode('R');
|
||||||
|
graph.addNode(r);
|
||||||
|
graph.addEdge('R', 'B');
|
||||||
|
r.reflect(graph);
|
||||||
|
|
||||||
|
const w = new RewritingNode('W');
|
||||||
|
graph.addNode(w);
|
||||||
|
graph.addEdge('W', 'C');
|
||||||
|
const d = new Node('D');
|
||||||
|
w.rewrite(graph, 'C', d);
|
||||||
|
|
||||||
|
const traversal = graph.traverse('A');
|
||||||
|
// Should visit A, B, D, and B_ref (which points to D)
|
||||||
|
expect(traversal).toContain('A');
|
||||||
|
expect(traversal).toContain('B');
|
||||||
|
expect(traversal).toContain('D');
|
||||||
|
expect(traversal).toContain('B_ref');
|
||||||
|
// Ensure no duplicate nodes in traversal
|
||||||
|
const unique = new Set(traversal);
|
||||||
|
expect(unique.size).toBe(traversal.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
+136
-2
@@ -1,2 +1,136 @@
|
|||||||
export { Graph } from './graph.js';
|
const { strict: assert } = require('assert');
|
||||||
export { Node, ReflectionNode, RewritingNode } from './nodes.js';
|
|
||||||
|
class Node {
|
||||||
|
constructor(id, type = 'generic') {
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReflectionNode extends Node {
|
||||||
|
constructor(id) {
|
||||||
|
super(id, 'reflection');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reflects all outgoing edges of this node by creating copies of the target nodes.
|
||||||
|
* @param {Graph} graph - The graph instance to operate on.
|
||||||
|
*/
|
||||||
|
reflect(graph) {
|
||||||
|
const targets = graph.edges.get(this.id) || new Set();
|
||||||
|
for (const targetId of targets) {
|
||||||
|
const targetNode = graph.getNode(targetId);
|
||||||
|
if (!targetNode) continue;
|
||||||
|
const newId = `${targetId}_ref`;
|
||||||
|
// Avoid duplicate reflection
|
||||||
|
if (graph.getNode(newId)) continue;
|
||||||
|
const newNode = new Node(newId, targetNode.type);
|
||||||
|
graph.addNode(newNode);
|
||||||
|
const targetTargets = graph.edges.get(targetId) || new Set();
|
||||||
|
for (const tt of targetTargets) {
|
||||||
|
graph.addEdge(newId, tt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RewritingNode extends Node {
|
||||||
|
constructor(id) {
|
||||||
|
super(id, 'rewriting');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrites a target node in the graph with a new node.
|
||||||
|
* @param {Graph} graph - The graph instance to operate on.
|
||||||
|
* @param {string} targetId - The id of the node to replace.
|
||||||
|
* @param {Node} newNode - The new node that will replace the target.
|
||||||
|
*/
|
||||||
|
rewrite(graph, targetId, newNode) {
|
||||||
|
graph.replaceNode(targetId, newNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Graph {
|
||||||
|
constructor() {
|
||||||
|
this.nodes = new Map(); // id -> Node
|
||||||
|
this.edges = new Map(); // id -> Set of target ids
|
||||||
|
}
|
||||||
|
|
||||||
|
addNode(node) {
|
||||||
|
assert(node && node.id, 'Node must have an id');
|
||||||
|
this.nodes.set(node.id, node);
|
||||||
|
if (!this.edges.has(node.id)) {
|
||||||
|
this.edges.set(node.id, new Set());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(fromId, toId) {
|
||||||
|
assert(this.nodes.has(fromId), `Source node ${fromId} does not exist`);
|
||||||
|
assert(this.nodes.has(toId), `Target node ${toId} does not exist`);
|
||||||
|
if (!this.edges.has(fromId)) {
|
||||||
|
this.edges.set(fromId, new Set());
|
||||||
|
}
|
||||||
|
this.edges.get(fromId).add(toId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNode(id) {
|
||||||
|
return this.nodes.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces an existing node with a new node, preserving edges.
|
||||||
|
* @param {string} oldId - The id of the node to replace.
|
||||||
|
* @param {Node} newNode - The new node that will replace the old one.
|
||||||
|
*/
|
||||||
|
replaceNode(oldId, newNode) {
|
||||||
|
if (!this.nodes.has(oldId)) {
|
||||||
|
throw new Error(`Node ${oldId} not found`);
|
||||||
|
}
|
||||||
|
const oldTargets = this.edges.get(oldId) ? new Set(this.edges.get(oldId)) : new Set();
|
||||||
|
|
||||||
|
// Remove old node and its edges
|
||||||
|
this.edges.delete(oldId);
|
||||||
|
this.nodes.delete(oldId);
|
||||||
|
|
||||||
|
// Add new node
|
||||||
|
this.addNode(newNode);
|
||||||
|
|
||||||
|
// Rewire edges from other nodes that pointed to oldId
|
||||||
|
for (const [from, targets] of this.edges.entries()) {
|
||||||
|
if (targets.has(oldId)) {
|
||||||
|
targets.delete(oldId);
|
||||||
|
targets.add(newNode.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add edges from new node to oldTargets
|
||||||
|
for (const target of oldTargets) {
|
||||||
|
this.addEdge(newNode.id, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depth-first traversal starting from a node.
|
||||||
|
* @param {string} startId - The starting node id.
|
||||||
|
* @param {Set<string>} visited - Internal set to track visited nodes.
|
||||||
|
* @returns {string[]} - Array of visited node ids in traversal order.
|
||||||
|
*/
|
||||||
|
traverse(startId, visited = new Set()) {
|
||||||
|
if (!this.nodes.has(startId)) return [];
|
||||||
|
if (visited.has(startId)) return [];
|
||||||
|
visited.add(startId);
|
||||||
|
const result = [startId];
|
||||||
|
const targets = this.edges.get(startId) || new Set();
|
||||||
|
for (const t of targets) {
|
||||||
|
result.push(...this.traverse(t, visited));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Node,
|
||||||
|
ReflectionNode,
|
||||||
|
RewritingNode,
|
||||||
|
Graph,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user