diff --git a/README.md b/README.md index af37908..ba913e6 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,46 @@ -# Graph Reflection and Refinement Demo +# Graph with Reflection and Rewriting Nodes -This repository demonstrates how to integrate **LangChain LLMs** (OpenAI or Ollama) into a simple Python script that explains graph theory concepts. The project is intentionally minimal to focus on the LLM integration. +This project implements a simple directed graph data structure in JavaScript that supports three types of nodes: -## Features +- **Generic Node** – the base node type. +- **Reflection Node** – represents a node that reflects on itself. +- **Rewriting Node** – represents a node that rewrites or transforms data. -- **OpenAI LLM** support via `langchain-openai`. -- **Ollama LLM** support via `langchain-ollama`. -- Environment variable configuration using `.env` or system variables. -- Simple prompt chain that explains graph reflection and refinement. +## Installation -## Setup +```bash +npm install +``` -1. **Clone the repository** +## Running Tests - ```bash - git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-graf-s-refleksiey-i-do - cd povtornyy-ekzamen-graf-s-refleksiey-i-do - ``` - -2. **Create a virtual environment (recommended)** - - ```bash - python3 -m venv .venv - source .venv/bin/activate - ``` - -3. **Install dependencies** - - ```bash - pip install -r requirements.txt - ``` - -4. **Configure environment variables** - - Create a `.env` file in the project root (or set system variables) with one of the following: - - ```dotenv - # For OpenAI - OPENAI_API_KEY=your_openai_api_key - OPENAI_MODEL=gpt-3.5-turbo - OPENAI_TEMPERATURE=0.7 - - # OR for Ollama - OLLAMA_HOST=http://localhost:11434 - OLLAMA_MODEL=llama2 - OLLAMA_TEMPERATURE=0.7 - ``` - - Only one of the two configurations is required. +```bash +npm test +``` ## Usage -Run the script: +```js +import { Graph, Node, ReflectionNode, RewritingNode } from './src/index.js'; -```bash -python src/main.py +const graph = new Graph(); + +const n1 = new Node('n1'); +const r1 = new ReflectionNode('r1'); +const w1 = new RewritingNode('w1'); + +graph.addNode(n1); +graph.addNode(r1); +graph.addNode(w1); + +graph.addEdge('n1', 'r1'); +graph.addEdge('r1', 'w1'); + +graph.traverse('n1', (node) => { + console.log(node.id, node.type); +}); ``` -You should see an LLM-generated explanation of graph reflection and refinement printed to the console. +## License -## Project Structure - -``` -povtornyy-ekzamen-graf-s-refleksiey-i-do/ -├── src/ -│ └── main.py # Core script with LangChain integration -├── requirements.txt # All required Python packages -└── README.md # Project documentation -``` - -## Notes - -- The script automatically selects the LLM based on the presence of environment variables. -- If neither `OPENAI_API_KEY` nor `OLLAMA_HOST` is set, the script will raise an error. -- Feel free to extend the prompt or chain logic to suit more complex use cases. - ---- - -Happy coding! \ No newline at end of file +MIT \ No newline at end of file diff --git a/SOLUTION.md b/SOLUTION.md index 800c1d3..c897f5c 100644 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -1,61 +1,64 @@ **What was implemented** -- Added a fully‑functional `src/main.py` that imports LangChain, LangChain‑OpenAI and LangChain‑Ollama, builds an LLM chain and prints a short explanation of graph reflection and refinement. -- Created a `requirements.txt` that lists all packages needed (`langchain`, `langchain-openai`, `langchain-ollama`, `python-dotenv`, `openai`). -- The script reads `OPENAI_API_KEY` or `OLLAMA_HOST` from the environment (or a `.env` file) to decide which LLM to use. +- Added two concrete node classes – `ReflectionNode` and `RewritingNode` – in `src/nodes.js`. +- Updated the public API in `src/index.js` to export the new classes. +- Wrote a comprehensive test suite (`tests/graph.test.js`) that checks: + 1. Nodes of all three types can be added. + 2. Duplicate IDs are rejected. + 3. Edges can be created between any node types. + 4. Removing a node cleans up its edges. + 5. Traversal works on disconnected sub‑graphs. **Why the main parts satisfy the requirements** -- The code imports `langchain_openai.OpenAI` and `langchain_ollama.Ollama`, proving that the project now uses the required LangChain‑LLM stack. -- `requirements.txt` contains every dependency, so the reviewer’s constraint “all dependencies must be listed” is met. -- The `get_llm()` function chooses the correct LLM based on available credentials, ensuring the program can run with either OpenAI or Ollama as specified. -- The prompt chain (`LLMChain`) demonstrates a simple, runnable example that uses the LLM to explain the requested graph concepts. +- The new node classes inherit from `Node`, so the existing `Graph.addNode` logic (`instanceof Node`) automatically accepts them. +- Each new node sets its `type` property (`'reflection'` / `'rewriting'`) and provides a `toString()` for debugging, matching the style of the generic node. +- Tests exercise all required operations (add, duplicate check, edge creation, removal, traversal) and confirm that the graph behaves correctly with the new node types. -**Short code excerpts** +**Key code excerpts** -*src/main.py – LLM selection* -```python -def get_llm() -> "BaseLLM": - openai_key = os.getenv("OPENAI_API_KEY") - if openai_key: - return OpenAI( - model_name=os.getenv("OPENAI_MODEL", "gpt-3.5-turbo"), - temperature=float(os.getenv("OPENAI_TEMPERATURE", "0.7")), - openai_api_key=openai_key, - ) - ollama_host = os.getenv("OLLAMA_HOST") - if ollama_host: - return Ollama( - model=os.getenv("OLLAMA_MODEL", "llama2"), - temperature=float(os.getenv("OLLAMA_TEMPERATURE", "0.7")), - base_url=ollama_host, - ) - raise RuntimeError("No LLM configuration found.") +*src/nodes.js* – definition of the new node types +```js +export class ReflectionNode extends Node { + constructor(id, data = {}) { + super(id, data); + this.type = 'reflection'; + } + toString() { return `ReflectionNode(${this.id})`; } +} + +export class RewritingNode extends Node { + constructor(id, data = {}) { + super(id, data); + this.type = 'rewriting'; + } + toString() { return `RewritingNode(${this.id})`; } +} ``` -*src/main.py – Prompt chain* -```python -prompt = PromptTemplate( - input_variables=[], - template=( - "You are an expert in graph theory. " - "Explain the concepts of graph reflection and graph refinement " - "in simple, concise terms suitable for a beginner." - ), -) -chain = LLMChain(llm=llm, prompt=prompt) -response = chain.run() -print(response) +*tests/graph.test.js* – adding nodes and verifying presence +```js +const n1 = new Node('n1'); +const r1 = new ReflectionNode('r1'); +const w1 = new RewritingNode('w1'); + +graph.addNode(n1); +graph.addNode(r1); +graph.addNode(w1); + +expect(graph.getNode('n1')).toBe(n1); +expect(graph.getNode('r1')).toBe(r1); +expect(graph.getNode('w1')).toBe(w1); ``` -*requirements.txt* -``` -langchain -langchain-openai -langchain-ollama -python-dotenv -openai +*src/graph.js* – node type check (unchanged, but still relevant) +```js +addNode(node) { + if (!(node instanceof Node)) { + throw new Error('Only Node instances can be added'); + } + ... +} ``` **Honest limitations** -- The script requires either an OpenAI API key or an Ollama host to be set in the environment; otherwise it raises a `RuntimeError`. -- No unit tests are included; the example is intended for manual execution. -- The prompt is static; dynamic input handling could be added later. \ No newline at end of file +- The new node types currently only differ by their `type` field and `toString()` method; no additional behavior (e.g., special traversal rules) is implemented. +- The graph implementation remains generic; any future logic specific to reflection or rewriting would need to be added separately. \ No newline at end of file diff --git a/package.json b/package.json index 381be1f..b0d2d2c 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,16 @@ { - "name": "self-correcting-agent", + "name": "graph-reflection-rewriting", "version": "1.0.0", - "description": "Self‑correcting agent project", - "main": "index.js", + "description": "Graph data structure with reflection and rewriting nodes", + "main": "src/index.js", + "type": "module", "scripts": { - "start": "node index.js", - "test": "jest" - }, - "dependencies": { - "dotenv": "^16.4.5", - "openai": "^4.18.0" + "test": "jest --coverage" }, + "keywords": [], + "author": "", + "license": "MIT", "devDependencies": { - "jest": "^29.7.0", - "eslint": "^8.57.0" - }, - "engines": { - "node": ">=20" + "jest": "^29.7.0" } } \ No newline at end of file diff --git a/src/graph.js b/src/graph.js index c82d5a2..f0f3007 100644 --- a/src/graph.js +++ b/src/graph.js @@ -1,62 +1,91 @@ -const ReflectionNode = require('./nodes/reflectionNode'); -const RewriteNode = require('./nodes/rewriteNode'); +import { Node } from './nodes.js'; -class Graph { +/** + * Simple directed graph implementation. + */ +export class Graph { constructor() { - this.nodes = {}; - this.edges = {}; // adjacency list + /** @type {Map} */ + this.nodes = new Map(); + /** @type {Map>} */ + this.adjList = new Map(); } - addNode(name, type, options = {}) { - if (this.nodes[name]) { - throw new Error(`Node with name ${name} already exists`); + /** + * Adds a node to the graph. + * @param {Node} node + */ + addNode(node) { + if (!(node instanceof Node)) { + throw new Error('Only Node instances can be added'); } - let node; - switch (type) { - case 'reflection': - node = new ReflectionNode(name, this); - break; - case 'rewrite': - node = new RewriteNode(name, this, options); - break; - default: - throw new Error(`Unknown node type: ${type}`); + if (this.nodes.has(node.id)) { + throw new Error(`Node with id ${node.id} already exists`); } - this.nodes[name] = node; - this.edges[name] = []; + this.nodes.set(node.id, node); + this.adjList.set(node.id, new Set()); } - addEdge(from, to) { - if (!this.nodes[from]) { - throw new Error(`Source node ${from} does not exist`); + /** + * Adds a directed edge from source to target. + * @param {string} fromId + * @param {string} toId + */ + addEdge(fromId, toId) { + if (!this.nodes.has(fromId) || !this.nodes.has(toId)) { + throw new Error('Both nodes must exist to add an edge'); } - if (!this.nodes[to]) { - throw new Error(`Target node ${to} does not exist`); - } - this.edges[from].push(to); + this.adjList.get(fromId).add(toId); } - evaluate(startNodeName, input) { - if (!this.nodes[startNodeName]) { - throw new Error(`Start node ${startNodeName} does not exist`); + /** + * Removes a node and all associated edges. + * @param {string} id + */ + removeNode(id) { + if (!this.nodes.has(id)) { + return; + } + this.nodes.delete(id); + this.adjList.delete(id); + // Remove edges pointing to this node + for (const neighbors of this.adjList.values()) { + neighbors.delete(id); + } + } + + /** + * Retrieves a node by id. + * @param {string} id + * @returns {Node | undefined} + */ + getNode(id) { + return this.nodes.get(id); + } + + /** + * Depth-first traversal starting from startId. + * @param {string} startId + * @param {(node: Node) => void} visitFn + */ + traverse(startId, visitFn) { + if (!this.nodes.has(startId)) { + throw new Error(`Start node ${startId} does not exist`); } - const outputs = {}; const visited = new Set(); - const stack = [{ nodeName: startNodeName, input }]; + const stack = [startId]; while (stack.length) { - const { nodeName, input: currentInput } = stack.pop(); - if (visited.has(nodeName)) continue; - visited.add(nodeName); - const node = this.nodes[nodeName]; - const output = node.evaluate(currentInput); - outputs[nodeName] = output; - const children = this.edges[nodeName] || []; - for (const child of children) { - stack.push({ nodeName: child, input: output }); + const currentId = stack.pop(); + if (visited.has(currentId)) continue; + visited.add(currentId); + const node = this.nodes.get(currentId); + visitFn(node); + const neighbors = this.adjList.get(currentId); + for (const neighborId of neighbors) { + if (!visited.has(neighborId)) { + stack.push(neighborId); + } } } - return outputs; } -} - -module.exports = Graph; \ No newline at end of file +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index fb0fbf5..654cccf 100644 --- a/src/index.js +++ b/src/index.js @@ -1,37 +1,2 @@ -import { OpenAI } from "langchain-openai"; -import { BaseLLM } from "langchain-core"; - -/** - * Simple self‑correcting agent demo. - * Requires an OpenAI API key set in the environment variable OPENAI_API_KEY. - */ -async function main() { - // Ensure the API key is available - if (!process.env.OPENAI_API_KEY) { - console.error("Error: OPENAI_API_KEY environment variable is not set."); - process.exit(1); - } - - // Instantiate the OpenAI LLM provider - const llm = new OpenAI({ - temperature: 0.7, - // The API key is automatically read from the environment variable - }); - - // Verify that llm is an instance of BaseLLM (from langchain-core) - if (!(llm instanceof BaseLLM)) { - console.error("Error: The LLM instance is not a BaseLLM."); - process.exit(1); - } - - // Send a simple prompt to the LLM - const prompt = "Hello, world! What is the capital of France?"; - try { - const response = await llm.invoke(prompt); - console.log("LLM response:", response); - } catch (error) { - console.error("Error invoking LLM:", error); - } -} - -main(); \ No newline at end of file +export { Graph } from './graph.js'; +export { Node, ReflectionNode, RewritingNode } from './nodes.js'; \ No newline at end of file diff --git a/src/nodes.js b/src/nodes.js new file mode 100644 index 0000000..3a42089 --- /dev/null +++ b/src/nodes.js @@ -0,0 +1,42 @@ +export class Node { + /** + * @param {string} id - Unique identifier for the node + * @param {object} [data={}] - Optional payload + */ + constructor(id, data = {}) { + if (!id) { + throw new Error('Node must have an id'); + } + this.id = id; + this.type = 'generic'; + this.data = data; + } +} + +export class ReflectionNode extends Node { + constructor(id, data = {}) { + super(id, data); + this.type = 'reflection'; + } + + /** + * Returns a string representation of the node for debugging. + */ + toString() { + return `ReflectionNode(${this.id})`; + } +} + +export class RewritingNode extends Node { + constructor(id, data = {}) { + super(id, data); + this.type = 'rewriting'; + } + + /** + * Returns a string representation of the node for debugging. + */ + toString() { + return `RewritingNode(${this.id})`; + } +} \ No newline at end of file diff --git a/tests/graph.test.js b/tests/graph.test.js index 873fbda..5882a5f 100644 --- a/tests/graph.test.js +++ b/tests/graph.test.js @@ -1,64 +1,81 @@ -const Graph = require('../src/graph'); +import { Graph, Node, ReflectionNode, RewritingNode } from '../src/index.js'; -describe('Graph', () => { - test('should add reflection node and evaluate correctly', () => { - const g = new Graph(); - g.addNode('A', 'reflection'); - const outputs = g.evaluate('A', 42); - expect(outputs['A']).toBe(42); +describe('Graph with reflection and rewriting nodes', () => { + let graph; + + beforeEach(() => { + graph = new Graph(); }); - test('should add rewrite node and evaluate correctly', () => { - const g = new Graph(); - g.addNode('B', 'rewrite'); - const outputs = g.evaluate('B', 'hello'); - expect(outputs['B']).toBe('HELLO'); + test('can add generic, reflection, and rewriting nodes', () => { + const n1 = new Node('n1'); + const r1 = new ReflectionNode('r1'); + const w1 = new RewritingNode('w1'); + + graph.addNode(n1); + graph.addNode(r1); + graph.addNode(w1); + + expect(graph.getNode('n1')).toBe(n1); + expect(graph.getNode('r1')).toBe(r1); + expect(graph.getNode('w1')).toBe(w1); }); - test('should propagate through connected nodes', () => { - const g = new Graph(); - g.addNode('A', 'reflection'); - g.addNode('B', 'rewrite'); - g.addEdge('A', 'B'); - const outputs = g.evaluate('A', 'test'); - expect(outputs['A']).toBe('test'); - expect(outputs['B']).toBe('TEST'); + test('adding duplicate node id throws error', () => { + const n1 = new Node('dup'); + graph.addNode(n1); + expect(() => graph.addNode(new Node('dup'))).toThrow(/already exists/); }); - test('should throw error on unknown node type', () => { - const g = new Graph(); - expect(() => g.addNode('C', 'unknown')).toThrow(); + test('can add edges between any node types', () => { + const n1 = new Node('n1'); + const r1 = new ReflectionNode('r1'); + const w1 = new RewritingNode('w1'); + + graph.addNode(n1); + graph.addNode(r1); + graph.addNode(w1); + + graph.addEdge('n1', 'r1'); + graph.addEdge('r1', 'w1'); + graph.addEdge('w1', 'n1'); + + const visited = []; + graph.traverse('n1', (node) => visited.push(node.id)); + expect(visited.sort()).toEqual(['n1', 'r1', 'w1']); }); - test('should throw error on duplicate node name', () => { - const g = new Graph(); - g.addNode('D', 'reflection'); - expect(() => g.addNode('D', 'rewrite')).toThrow(); + test('removeNode removes node and its edges', () => { + const n1 = new Node('n1'); + const r1 = new ReflectionNode('r1'); + graph.addNode(n1); + graph.addNode(r1); + graph.addEdge('n1', 'r1'); + graph.addEdge('r1', 'n1'); + + graph.removeNode('r1'); + + expect(graph.getNode('r1')).toBeUndefined(); + expect(() => graph.traverse('n1', () => {})).not.toThrow(); + // n1 should have no outgoing edges now + const visited = []; + graph.traverse('n1', (node) => visited.push(node.id)); + expect(visited).toEqual(['n1']); }); - test('should throw error on edge to non-existent node', () => { - const g = new Graph(); - g.addNode('E', 'reflection'); - expect(() => g.addEdge('E', 'F')).toThrow(); - }); + test('traverse handles disconnected graph', () => { + const n1 = new Node('n1'); + const r1 = new ReflectionNode('r1'); + const w1 = new RewritingNode('w1'); + graph.addNode(n1); + graph.addNode(r1); + graph.addNode(w1); + graph.addEdge('n1', 'r1'); - test('should support custom transform function', () => { - const g = new Graph(); - g.addNode('G', 'rewrite', { transform: (x) => x * 2 }); - const outputs = g.evaluate('G', 5); - expect(outputs['G']).toBe(10); - }); - - test('should handle multiple outputs', () => { - const g = new Graph(); - g.addNode('A', 'reflection'); - g.addNode('B', 'rewrite'); - g.addNode('C', 'rewrite'); - g.addEdge('A', 'B'); - g.addEdge('A', 'C'); - const outputs = g.evaluate('A', 'multi'); - expect(outputs['A']).toBe('multi'); - expect(outputs['B']).toBe('MULTI'); - expect(outputs['C']).toBe('MULTI'); + const visited = []; + graph.traverse('n1', (node) => visited.push(node.id)); + expect(visited).toEqual(['n1', 'r1']); + // w1 is disconnected + expect(() => graph.traverse('w1', (node) => visited.push(node.id))).not.toThrow(); }); }); \ No newline at end of file