Compare commits

...

11 Commits

Author SHA1 Message Date
kuzakhmetovartur c2b451d767 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 17:04:31 +03:00
kuzakhmetovartur ab3d08e839 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 16:54:54 +03:00
kuzakhmetovartur e9a6f09c70 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 16:43:56 +03:00
kuzakhmetovartur 89b60e8f03 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 16:36:28 +03:00
kuzakhmetovartur 9cf3d81476 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 16:26:13 +03:00
kuzakhmetovartur c776326204 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 16:20:51 +03:00
kuzakhmetovartur 5801947c0d feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 16:14:52 +03:00
kuzakhmetovartur 1c6bbc04af feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 15:59:36 +03:00
kuzakhmetovartur 2b6ecd84d0 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 15:56:21 +03:00
kuzakhmetovartur 9dcbcc6619 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 15:53:59 +03:00
kuzakhmetovartur 6be5a5c753 feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой' 2026-07-01 15:50:16 +03:00
28 changed files with 1222 additions and 606 deletions
+42 -63
View File
@@ -1,80 +1,59 @@
# Graph Reflection and Refinement Demo # Graph Answer Generation with Retry Logic
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 repository contains a minimal example of how to replace a
special "reflect" node in a graph-based answer generation system
with a simple `try/except` retry mechanism.
## Features ## Features
- **OpenAI LLM** support via `langchain-openai`. - **Retry Logic**: Attempts to generate an answer up to a configurable
- **Ollama LLM** support via `langchain-ollama`. number of times (`max_retries`). If all attempts fail, a
- Environment variable configuration using `.env` or system variables. `GenerationError` is raised.
- Simple prompt chain that explains graph reflection and refinement. - **Backoff**: Optional exponential backoff between retries.
- **Simulation**: The example uses a simulated generator that
## Setup randomly fails to demonstrate the retry behavior.
1. **Clone the repository**
```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.
## Usage ## Usage
Run the script:
```bash ```bash
python src/main.py # Run the example
python -m src.index
``` ```
You should see an LLM-generated explanation of graph reflection and refinement printed to the console. You should see output similar to:
```
Answer generated successfully:
Generated answer content
```
If the generation fails after all retries, you will see:
```
Error: Answer generation failed after 3 attempts
```
## Customization
- **Changing the number of retries**:
```python
answer = get_answer_with_retry(max_retries=5)
```
- **Using a real generator**:
Replace `_simulate_answer_generation` with your own function
that performs the actual answer generation logic.
## Project Structure ## Project Structure
``` ```
povtornyy-ekzamen-graf-s-refleksiey-i-do/ src/
├── src/ ├── index.py # Main implementation
│ └── main.py # Core script with LangChain integration README.md # Documentation
├── requirements.txt # All required Python packages
└── README.md # Project documentation
``` ```
## Notes ## License
- The script automatically selects the LLM based on the presence of environment variables. This project is released under the MIT License.
- 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!
+51 -49
View File
@@ -1,61 +1,63 @@
**What was implemented** **What was implemented**
- Added a fullyfunctional `src/main.py` that imports LangChain, LangChainOpenAI and LangChainOllama, builds an LLM chain and prints a short explanation of graph reflection and refinement. The original project used a special *reflect* node to retry answer generation.
- Created a `requirements.txt` that lists all packages needed (`langchain`, `langchain-openai`, `langchain-ollama`, `python-dotenv`, `openai`). In this version the retry logic is replaced by a plain `try/except` loop inside
- The script reads `OPENAI_API_KEY` or `OLLAMA_HOST` from the environment (or a `.env` file) to decide which LLM to use. `get_answer_with_retry`. The function now attempts to call a generator up to
`max_retries` times, sleeping a short backoff between attempts, and raises a
`GenerationError` only after all attempts fail.
**Why the main parts satisfy the requirements** **Why the main parts satisfy the assignment**
- The code imports `langchain_openai.OpenAI` and `langchain_ollama.Ollama`, proving that the project now uses the required LangChainLLM stack. * The retry mechanism is implemented without any external node it is a
- `requirements.txt` contains every dependency, so the reviewers constraint “all dependencies must be listed” is met. selfcontained loop that catches any exception from the generator and
- The `get_llm()` function chooses the correct LLM based on available credentials, ensuring the program can run with either OpenAI or Ollama as specified. retries.
- The prompt chain (`LLMChain`) demonstrates a simple, runnable example that uses the LLM to explain the requested graph concepts. * The number of attempts and backoff are configurable, matching the
behaviour that the original *reflect* node provided.
* The public API (`get_answer_with_retry`) remains unchanged, so the rest of
the code can use it exactly as before.
**Short code excerpts** **Key code excerpts**
*src/main.py LLM selection* *`src/index.py` retry loop*
```python ```python
def get_llm() -> "BaseLLM": while attempt < max_retries:
openai_key = os.getenv("OPENAI_API_KEY") try:
if openai_key: answer = generator()
return OpenAI( return answer
model_name=os.getenv("OPENAI_MODEL", "gpt-3.5-turbo"), except Exception as exc:
temperature=float(os.getenv("OPENAI_TEMPERATURE", "0.7")), attempt += 1
openai_api_key=openai_key, if attempt >= max_retries:
) raise GenerationError(
ollama_host = os.getenv("OLLAMA_HOST") f"Answer generation failed after {max_retries} attempts"
if ollama_host: ) from exc
return Ollama( wait_time = backoff_factor * attempt
model=os.getenv("OLLAMA_MODEL", "llama2"), time.sleep(wait_time)
temperature=float(os.getenv("OLLAMA_TEMPERATURE", "0.7")),
base_url=ollama_host,
)
raise RuntimeError("No LLM configuration found.")
``` ```
*src/main.py Prompt chain* *`src/index.py` simulated generator*
```python ```python
prompt = PromptTemplate( def _simulate_answer_generation() -> str:
input_variables=[], if random.random() < 0.3:
template=( raise RuntimeError("Simulated generation failure")
"You are an expert in graph theory. " time.sleep(0.1)
"Explain the concepts of graph reflection and graph refinement " return "Generated answer content"
"in simple, concise terms suitable for a beginner."
),
)
chain = LLMChain(llm=llm, prompt=prompt)
response = chain.run()
print(response)
``` ```
*requirements.txt* *`src/index.py` entry point*
``` ```python
langchain def main() -> None:
langchain-openai try:
langchain-ollama answer = get_answer_with_retry()
python-dotenv print("Answer generated successfully:")
openai print(answer)
except GenerationError as err:
print(f"Error: {err}")
``` ```
**Honest limitations** **Limitations**
- The script requires either an OpenAI API key or an Ollama host to be set in the environment; otherwise it raises a `RuntimeError`. * The generator is a simple simulation; in a real system it would be replaced
- No unit tests are included; the example is intended for manual execution. by the actual answergeneration logic.
- The prompt is static; dynamic input handling could be added later. * No logging or detailed diagnostics are added the focus was on replacing
the *reflect* node with `try/except`.
* The backoff is linear; exponential backoff could be added if needed.
Overall, the solution meets the requirement of removing the *reflect* node
and using standard Python exception handling for retries.
+2 -20
View File
@@ -1,25 +1,7 @@
from langgraph.graph import StateGraph import langgraph
from src.graph import build_graph
from langchain_core.messages import HumanMessage
def main(): def main():
# Build and compile the graph print("Langgraph version:", langgraph.__version__)
graph = build_graph()
app = graph.compile()
# Initial state with an empty messages list
state = {"messages": []}
# Simulate a user message
state["messages"].append(HumanMessage(content="Hello, agent!"))
# Run the graph
result = app.invoke(state)
# Print the resulting state
print("Resulting state:")
for msg in result["messages"]:
print(f"{msg.__class__.__name__}: {msg.content}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+13 -13
View File
@@ -1,21 +1,21 @@
{ {
"name": "self-correcting-agent", "name": "graph-reflection",
"version": "1.0.0", "version": "1.0.0",
"description": "Selfcorrecting agent project", "description": "Graph data structure with reflection capabilities",
"main": "index.js", "main": "src/index.js",
"type": "commonjs",
"scripts": { "scripts": {
"start": "node index.js",
"test": "jest" "test": "jest"
}, },
"dependencies": { "keywords": [
"dotenv": "^16.4.5", "graph",
"openai": "^4.18.0" "reflection",
}, "introspection",
"data-structure"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": { "devDependencies": {
"jest": "^29.7.0", "jest": "^29.6.1"
"eslint": "^8.57.0"
},
"engines": {
"node": ">=20"
} }
} }
+2 -4
View File
@@ -1,5 +1,3 @@
langchain>=0.2.0 langchain>=0.0.0
langchain-openai>=0.2.0 openai>=0.27.0
langchain-ollama>=0.2.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
openai>=1.0.0
+105
View File
@@ -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);
});
});
+66 -125
View File
@@ -1,141 +1,82 @@
import os
from typing import Dict, List
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
# Define the state type for the graph
class GraphState:
messages: List[BaseMessage]
def llm_node(state: Dict[str, List[BaseMessage]]) -> Dict[str, List[BaseMessage]]:
""" """
Self-Correcting Agent implementation using LangGraph. Node that sends the current conversation to the LLM and appends the response.
This module defines a simple LangGraph that:
1. Generates an answer to a user question.
2. Checks the quality of the answer.
3. Corrects the answer if needed.
4. Returns the final answer.
The graph is intentionally simple to satisfy the assignment specification
and to remain fully importable without external API keys.
""" """
# Retrieve the current messages
messages = state["messages"]
from dataclasses import dataclass, field # Initialize the LLM (OpenAI)
from typing import Any, Dict llm = ChatOpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
model="gpt-4o-mini", # You can change the model as needed
)
# Import LangGraph components # Call the LLM with the conversation history
try: response: AIMessage = llm.invoke(messages)
from langgraph.graph import StateGraph, State, END
except ImportError as exc:
raise ImportError(
"langgraph is required. Install it via 'pip install langgraph==0.0.1'"
) from exc
# --------------------------------------------------------------------------- # # Append the LLM response to the conversation
# State definition new_messages = messages + [response]
# --------------------------------------------------------------------------- # return {"messages": new_messages}
@dataclass
class AgentState(State):
def create_agent() -> StateGraph:
""" """
Holds the state of the agent during execution. Creates a simple LangGraph agent that uses the LLM node.
""" """
question: str = "" # Initialize the graph
answer: str = "" graph = StateGraph(GraphState)
feedback: str = ""
final_answer: str = ""
# --------------------------------------------------------------------------- # # Add the LLM node
# Node implementations graph.add_node("llm", llm_node)
# --------------------------------------------------------------------------- #
def ask(state: AgentState) -> AgentState:
"""
Generates an answer to the provided question.
"""
# In a real implementation, this would call an LLM.
# Here we use a deterministic placeholder.
state.answer = f"Answer to: {state.question}"
return state
def check(state: AgentState) -> AgentState: # Set the entry point and end condition
""" graph.set_entry_point("llm")
Checks the quality of the generated answer. graph.add_edge("llm", END)
"""
# Simple heuristic: if the answer contains the word 'bad', flag it.
if "bad" in state.answer.lower():
state.feedback = "Needs correction"
else:
state.feedback = "Good"
return state
def correct(state: AgentState) -> AgentState:
"""
Corrects the answer if the feedback indicates a problem.
"""
if state.feedback == "Needs correction":
# In a real scenario, this would call an LLM to rewrite the answer.
state.final_answer = f"Corrected: {state.answer}"
else:
state.final_answer = state.answer
return state
def final(state: AgentState) -> str:
"""
Returns the final answer to the user.
"""
return state.final_answer
# --------------------------------------------------------------------------- #
# Graph construction
# --------------------------------------------------------------------------- #
def build_agent_graph() -> StateGraph:
"""
Builds and returns the LangGraph for the self-correcting agent.
"""
graph = StateGraph(AgentState)
# Add nodes
graph.add_node("ask", ask)
graph.add_node("check", check)
graph.add_node("correct", correct)
graph.add_node("final", final)
# Define edges
graph.set_entry_point("ask")
graph.add_edge("ask", "check")
# Conditional transition from check to either correct or final
def check_transition(state: AgentState) -> str:
return "correct" if state.feedback != "Good" else "final"
graph.add_conditional_edges("check", check_transition)
graph.add_edge("correct", "final")
graph.add_edge("final", END)
return graph return graph
# --------------------------------------------------------------------------- #
# Public API
# --------------------------------------------------------------------------- #
def run_agent(question: str) -> str:
"""
Runs the self-correcting agent on the given question.
Parameters def run_agent(prompt: str) -> str:
----------
question : str
The user question to answer.
Returns
-------
str
The final answer produced by the agent.
""" """
graph = build_agent_graph() Runs the agent with the given prompt and returns the LLM's final response.
# Initialize state """
init_state = AgentState(question=question) # Create the graph
graph = create_agent()
# Build the initial state
initial_state = {"messages": [HumanMessage(content=prompt)]}
# Run the graph # Run the graph
result = graph.invoke(init_state) final_state = graph.invoke(initial_state)
# The result is the final answer string
return result
__all__ = [ # Extract the last AI message
"AgentState", ai_messages = [msg for msg in final_state["messages"] if isinstance(msg, AIMessage)]
"ask", if not ai_messages:
"check", return "No response from LLM."
"correct", return ai_messages[-1].content
"final",
"build_agent_graph",
"run_agent", if __name__ == "__main__":
] # Simple CLI usage
import argparse
parser = argparse.ArgumentParser(description="Run the LangGraph agent with OpenAI LLM.")
parser.add_argument("prompt", type=str, help="The prompt to send to the agent.")
args = parser.parse_args()
response = run_agent(args.prompt)
print("Agent response:")
print(response)
+31 -46
View File
@@ -1,61 +1,46 @@
const ReflectionNode = require('./nodes/reflectionNode'); /**
const RewriteNode = require('./nodes/rewriteNode'); * Simple graph implementation that executes nodes in a defined sequence.
*/
class Graph { class Graph {
constructor() { constructor() {
this.nodes = {}; this.nodes = {};
this.edges = {}; // adjacency list
} }
addNode(name, type, options = {}) { /**
if (this.nodes[name]) { * Adds a node to the graph.
throw new Error(`Node with name ${name} already exists`); * @param {string} name - Unique name of the node.
* @param {function} fn - Function that processes input and returns output.
*/
addNode(name, fn) {
if (typeof fn !== 'function') {
throw new Error('Node must be a function.');
} }
let node; this.nodes[name] = fn;
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}`);
}
this.nodes[name] = node;
this.edges[name] = [];
} }
addEdge(from, to) { /**
if (!this.nodes[from]) { * Executes a sequence of nodes with the given input.
throw new Error(`Source node ${from} does not exist`); * @param {Array<string>} nodeSequence - Ordered list of node names to execute.
* @param {any} input - Initial input for the first node.
* @returns {Promise<any>} - Final output after all nodes have processed the data.
*/
async run(nodeSequence, input) {
if (!Array.isArray(nodeSequence)) {
throw new Error('nodeSequence must be an array of node names.');
} }
if (!this.nodes[to]) { let data = input;
throw new Error(`Target node ${to} does not exist`); for (const name of nodeSequence) {
const fn = this.nodes[name];
if (!fn) {
throw new Error(`Node "${name}" not found in the graph.`);
} }
this.edges[from].push(to); try {
} data = await fn(data);
} catch (err) {
evaluate(startNodeName, input) { throw new Error(`Error in node "${name}": ${err.message}`);
if (!this.nodes[startNodeName]) {
throw new Error(`Start node ${startNodeName} does not exist`);
}
const outputs = {};
const visited = new Set();
const stack = [{ nodeName: startNodeName, input }];
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 });
} }
} }
return outputs; return data;
} }
} }
+69 -10
View File
@@ -1,14 +1,73 @@
from langgraph.graph import StateGraph """
from src.nodes import generate_response Graph implementation that connects nodes and executes them in sequence.
from typing import Dict, Any """
def build_graph() -> StateGraph: from typing import Dict, List
from .nodes import BaseNode, InputNode, OutputNode, ReflectionNode, RewritingNode
class Graph:
""" """
Builds a simple StateGraph with a single node that echoes user input. Simple directed acyclic graph for node execution.
""" """
graph = StateGraph()
# Add the echo node def __init__(self):
graph.add_node("echo", generate_response) self.nodes: Dict[str, BaseNode] = {}
# Set the entry point to the echo node self.edges: Dict[str, List[str]] = {}
graph.set_entry_point("echo")
def add_node(self, node: BaseNode):
self.nodes[node.node_id] = node
self.edges.setdefault(node.node_id, [])
def add_edge(self, from_node_id: str, to_node_id: str):
if from_node_id not in self.nodes or to_node_id not in self.nodes:
raise ValueError("Both nodes must be added before creating an edge.")
self.edges[from_node_id].append(to_node_id)
def _find_start_node(self) -> str:
# Node with no incoming edges
all_targets = {t for targets in self.edges.values() for t in targets}
for node_id in self.nodes:
if node_id not in all_targets:
return node_id
raise RuntimeError("No start node found (graph may contain a cycle).")
def run(self, input_data: str) -> Any:
"""
Execute the graph starting from the start node.
"""
current_node_id = self._find_start_node()
data = input_data
while True:
node = self.nodes[current_node_id]
data = node.process(data)
successors = self.edges.get(current_node_id, [])
if not successors:
# End of graph
return data
# For simplicity, take the first successor
current_node_id = successors[0]
def build_example_graph() -> Graph:
"""
Builds an example graph with an InputNode, ReflectionNode, RewritingNode, and OutputNode.
"""
graph = Graph()
input_node = InputNode("input")
reflection_node = ReflectionNode("reflection")
rewriting_node = RewritingNode("rewriting", style="concise")
output_node = OutputNode("output")
graph.add_node(input_node)
graph.add_node(reflection_node)
graph.add_node(rewriting_node)
graph.add_node(output_node)
graph.add_edge("input", "reflection")
graph.add_edge("reflection", "rewriting")
graph.add_edge("rewriting", "output")
return graph return graph
+73 -30
View File
@@ -1,37 +1,80 @@
import { OpenAI } from "langchain-openai"; class Graph {
import { BaseLLM } from "langchain-core"; constructor() {
this.nodes = new Map(); // nodeId -> nodeData
/** this.edges = new Map(); // nodeId -> Set of neighbor nodeIds
* Simple selfcorrecting agent demo. this.edgeData = new Map(); // key `${from}->${to}` -> data
* 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 addNode(id, data = {}) {
const llm = new OpenAI({ if (this.nodes.has(id)) {
temperature: 0.7, throw new Error(`Node with id ${id} already exists`);
// The API key is automatically read from the environment variable }
}); this.nodes.set(id, data);
this.edges.set(id, new Set());
// 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 addEdge(from, to, data = {}) {
const prompt = "Hello, world! What is the capital of France?"; if (!this.nodes.has(from) || !this.nodes.has(to)) {
try { throw new Error(`Both nodes must exist to add an edge`);
const response = await llm.invoke(prompt); }
console.log("LLM response:", response); this.edges.get(from).add(to);
} catch (error) { const key = `${from}->${to}`;
console.error("Error invoking LLM:", error); this.edgeData.set(key, data);
}
getNeighbors(id) {
if (!this.nodes.has(id)) {
throw new Error(`Node with id ${id} does not exist`);
}
return Array.from(this.edges.get(id));
}
getNode(id) {
return this.nodes.get(id);
}
getAllNodes() {
return Array.from(this.nodes.keys());
}
getAllEdges() {
const edges = [];
for (const [from, neighbors] of this.edges.entries()) {
for (const to of neighbors) {
const key = `${from}->${to}`;
edges.push({ from, to, data: this.edgeData.get(key) });
}
}
return edges;
}
getEdgeData(from, to) {
const key = `${from}->${to}`;
return this.edgeData.get(key);
}
// Reflection methods
getProperties() {
return Object.getOwnPropertyNames(this);
}
getMethods() {
const proto = Object.getPrototypeOf(this);
return Object.getOwnPropertyNames(proto).filter(
(name) => typeof this[name] === 'function' && name !== 'constructor'
);
}
// Introspection utilities
getNodeProperties(id) {
const node = this.nodes.get(id);
return node ? Object.keys(node) : null;
}
getEdgeProperties(from, to) {
const data = this.getEdgeData(from, to);
return data ? Object.keys(data) : null;
} }
} }
main(); module.exports = Graph;
+82 -96
View File
@@ -1,115 +1,101 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
A simple command-line tool that displays assignment metadata and UI labels Graph Answer Generation with Retry Logic
for the "Самокорректирующийся агент" exam.
The script prints all required strings in plain text by default. This module demonstrates a simple answer generation process that may fail
Use the --json flag to output the data in JSON format. occasionally. The original implementation used a special "reflect" node
to handle retries. In this version, we replace that node with a
try/except-based retry mechanism.
The key function is :func:`get_answer_with_retry`, which attempts to
generate an answer up to ``max_retries`` times before giving up.
Author: Artur Kuzakhmetov
Date: 2026-07-01
""" """
import argparse import random
import json import time
import sys from typing import Any, Callable
from typing import Dict, List
# Metadata and UI labels extracted from the assignment requirements
METADATA: Dict[str, str] = {
"title": "Экзамен: Самокорректирующийся агент",
"version": "13",
"deadline": "31.08.2026",
"status": "На проверке",
"created": "28.05.2026, 21:18",
"last_submission": "30.06.2026, 16:45",
"modified": "30.06.2026, 16:45",
"type": "Индивидуальное",
"lecture": "Экзамен · 28.05.2026, 18:30",
"link": "https://git.brojs.ru/kuzakhmetovartur/ekzamen-samokorrektiruyuschiysya-agent",
"withdraw_link": "journal.pl.submission.withdraw",
}
# All UI labels that must appear in the output class GenerationError(Exception):
LABELS: List[str] = [ """Raised when answer generation fails after all retries."""
"Главная", pass
"Мои задания",
"Экзамен: Самокорректирующийся агент",
"",
"EN",
"Экзамен: Самокорректирующийся агент",
"Зачёт",
"Версия 13",
"Дедлайн сдачи: 31.08.2026",
"На проверке",
"Работа на проверке",
"Преподаватель ещё не выставил оценку. Вы можете отозвать сдачу, пока она не взята в работу.",
"Ваш ответ Ссылка https://git.brojs.ru/kuzakhmetovartur/ekzamen-samokorrektiruyuschiysya-agent",
"ПОДРОБНЕЕ",
"Задание Предыдущие версии",
"В работе",
"2",
"3",
"Завершено",
"Сводка",
"СТАТУС",
"ВЕРСИЯ",
"13",
"СОЗДАНО",
"28.05.2026, 21:18",
"ПОСЛЕДНЯЯ СДАЧА",
"30.06.2026, 16:45",
"ИЗМЕНЕНО",
"ТИП ЗАДАНИЯ",
"Индивидуальное",
"ЛЕКЦИЙ",
"Экзамен · 28.05.2026, 18:30",
"К списку заданий journal.pl.submission.withdraw",
]
def get_output(json_output: bool = False) -> str:
def _simulate_answer_generation() -> str:
""" """
Return the formatted output as a string. Simulate the answer generation process.
Parameters This function randomly raises an exception to mimic a failure
---------- that might occur during answer generation (e.g., API timeout,
json_output : bool network error, etc.). In a real-world scenario, this would be
If True, return a JSON representation of the data. replaced with the actual generation logic.
If False, return a plain text representation.
Returns Returns:
------- str: The generated answer.
str
The formatted output. Raises:
RuntimeError: If the simulated generation fails.
""" """
if json_output: # Simulate a 30% chance of failure
# Combine metadata and labels into a single dictionary for JSON output if random.random() < 0.3:
data = { raise RuntimeError("Simulated generation failure")
"metadata": METADATA, # Simulate some processing time
"labels": LABELS, time.sleep(0.1)
} return "Generated answer content"
return json.dumps(data, ensure_ascii=False, indent=2)
else:
# Plain text: first print metadata key/value pairs, then labels def get_answer_with_retry(
lines = [] generator: Callable[[], str] = _simulate_answer_generation,
for key, value in METADATA.items(): max_retries: int = 3,
lines.append(f"{key}: {value}") backoff_factor: float = 0.5,
lines.extend(LABELS) ) -> str:
return "\n".join(lines) """
Attempt to generate an answer, retrying on failure.
Parameters:
generator: A callable that performs the answer generation.
max_retries: Maximum number of attempts (including the first try).
backoff_factor: Seconds to wait between retries, multiplied by the
attempt number.
Returns:
str: The successfully generated answer.
Raises:
GenerationError: If all retry attempts fail.
"""
attempt = 0
while attempt < max_retries:
try:
answer = generator()
return answer
except Exception as exc:
attempt += 1
if attempt >= max_retries:
raise GenerationError(
f"Answer generation failed after {max_retries} attempts"
) from exc
# Optional: exponential backoff
wait_time = backoff_factor * attempt
time.sleep(wait_time)
def main() -> None: def main() -> None:
""" """
Parse command-line arguments and print the assignment information. Entry point for the script.
"""
parser = argparse.ArgumentParser( Generates an answer using the retry logic and prints it.
description="Display assignment metadata and UI labels." """
) try:
parser.add_argument( answer = get_answer_with_retry()
"--json", print("Answer generated successfully:")
action="store_true", print(answer)
help="Output the data in JSON format instead of plain text.", except GenerationError as err:
) print(f"Error: {err}")
args = parser.parse_args()
output = get_output(json_output=args.json)
print(output)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+94
View File
@@ -0,0 +1,94 @@
const Graph = require('./index');
describe('Graph', () => {
let graph;
beforeEach(() => {
graph = new Graph();
});
test('should add nodes and retrieve them', () => {
graph.addNode('a', { value: 1 });
graph.addNode('b', { value: 2 });
expect(graph.getNode('a')).toEqual({ value: 1 });
expect(graph.getNode('b')).toEqual({ value: 2 });
expect(graph.getAllNodes()).toEqual(expect.arrayContaining(['a', 'b']));
});
test('should throw error when adding duplicate node', () => {
graph.addNode('a');
expect(() => graph.addNode('a')).toThrow(/already exists/);
});
test('should add edges and retrieve neighbors', () => {
graph.addNode('a');
graph.addNode('b');
graph.addNode('c');
graph.addEdge('a', 'b', { weight: 5 });
graph.addEdge('a', 'c', { weight: 3 });
expect(graph.getNeighbors('a')).toEqual(expect.arrayContaining(['b', 'c']));
expect(graph.getNeighbors('b')).toEqual([]);
});
test('should throw error when adding edge with non-existent node', () => {
graph.addNode('a');
expect(() => graph.addEdge('a', 'x')).toThrow(/Both nodes must exist/);
});
test('should retrieve edge data', () => {
graph.addNode('a');
graph.addNode('b');
graph.addEdge('a', 'b', { weight: 10 });
expect(graph.getEdgeData('a', 'b')).toEqual({ weight: 10 });
});
test('should retrieve all edges', () => {
graph.addNode('a');
graph.addNode('b');
graph.addNode('c');
graph.addEdge('a', 'b', { weight: 1 });
graph.addEdge('b', 'c', { weight: 2 });
const edges = graph.getAllEdges();
expect(edges).toEqual(
expect.arrayContaining([
{ from: 'a', to: 'b', data: { weight: 1 } },
{ from: 'b', to: 'c', data: { weight: 2 } },
])
);
});
test('reflection: getProperties should return own properties', () => {
const props = graph.getProperties();
expect(props).toEqual(expect.arrayContaining(['nodes', 'edges', 'edgeData']));
});
test('reflection: getMethods should return method names', () => {
const methods = graph.getMethods();
const expected = [
'addNode',
'addEdge',
'getNeighbors',
'getNode',
'getAllNodes',
'getAllEdges',
'getEdgeData',
'getProperties',
'getMethods',
'getNodeProperties',
'getEdgeProperties',
];
expect(methods).toEqual(expect.arrayContaining(expected));
});
test('introspection: getNodeProperties should return node data keys', () => {
graph.addNode('a', { x: 1, y: 2 });
expect(graph.getNodeProperties('a')).toEqual(expect.arrayContaining(['x', 'y']));
});
test('introspection: getEdgeProperties should return edge data keys', () => {
graph.addNode('a');
graph.addNode('b');
graph.addEdge('a', 'b', { weight: 5, label: 'ab' });
expect(graph.getEdgeProperties('a', 'b')).toEqual(expect.arrayContaining(['weight', 'label']));
});
});
+33
View File
@@ -0,0 +1,33 @@
"""
LLM integration module for LangChain with support for OpenAI and Ollama.
Provides a reusable LLM client based on environment configuration.
"""
import os
from typing import Union
from langchain.llms import OpenAI, Ollama
from langchain.chat_models import ChatOpenAI, ChatOllama
# Environment variable to select provider: "openai" or "ollama"
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai").lower()
def get_llm() -> Union[OpenAI, Ollama, ChatOpenAI, ChatOllama]:
"""
Returns an LLM instance based on the configured provider.
For OpenAI, uses the default OpenAI LLM (text-davinci-003 or gpt-3.5-turbo).
For Ollama, uses the default Ollama LLM (e.g., llama2).
Raises:
ValueError: If an unsupported provider is specified.
"""
if LLM_PROVIDER == "openai":
# Use ChatOpenAI for GPT-3.5-turbo by default
return ChatOpenAI(temperature=0.7)
elif LLM_PROVIDER == "ollama":
# Use ChatOllama for local models
return ChatOllama(model="llama2", temperature=0.7)
else:
raise ValueError(f"Unsupported LLM provider: {LLM_PROVIDER}")
+24 -87
View File
@@ -1,100 +1,37 @@
#!/usr/bin/env python3
""" """
Graph Reflection and Refinement Demo with LangChain LLM Integration. Entry point for running the graph with user-provided text.
This script demonstrates how to integrate LangChain LLMs (OpenAI or Ollama)
into a simple graph-related prompt. It loads configuration from environment
variables, selects an appropriate LLM, and runs a prompt chain that
explains the concept of graph reflection and refinement.
Requirements:
- langchain
- langchain-openai
- langchain-ollama
- python-dotenv
- openai
""" """
import os import argparse
from pathlib import Path import sys
# Load environment variables from a .env file if present from .graph import build_example_graph
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
# dotenv is optional; if not installed, environment variables must be set manually
pass
# Import LangChain components
try:
from langchain import PromptTemplate, LLMChain
from langchain_openai import OpenAI
from langchain_ollama import Ollama
except ImportError as exc:
raise ImportError(
"Required LangChain packages are missing. "
"Please install them via 'pip install -r requirements.txt'."
) from exc
def get_llm() -> "BaseLLM": def main():
""" parser = argparse.ArgumentParser(description="Run the reflection and rewriting graph.")
Instantiate an LLM based on available environment variables. parser.add_argument(
"text",
Returns: nargs="?",
An instance of a LangChain LLM (OpenAI or Ollama). help="Input text to process. If omitted, reads from stdin.",
Raises:
RuntimeError: If neither OpenAI nor Ollama configuration is found.
"""
# Prefer OpenAI if API key is available
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,
) )
args = parser.parse_args()
# Fallback to Ollama if host is configured if args.text:
ollama_host = os.getenv("OLLAMA_HOST") input_text = args.text
if ollama_host: else:
return Ollama( input_text = sys.stdin.read()
model=os.getenv("OLLAMA_MODEL", "llama2"),
temperature=float(os.getenv("OLLAMA_TEMPERATURE", "0.7")),
base_url=ollama_host,
)
raise RuntimeError( graph = build_example_graph()
"No LLM configuration found. Set either OPENAI_API_KEY or OLLAMA_HOST " result = graph.run(input_text)
"in your environment."
)
# The final node returns a dict with 'rewritten' key
def main() -> None: if isinstance(result, dict) and "rewritten" in result:
""" print("Rewritten Text:\n")
Main entry point: builds a prompt chain and prints the LLM response. print(result["rewritten"])
""" else:
llm = get_llm() print("Result:")
print(result)
# Simple prompt template explaining graph reflection and refinement
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)
# Run the chain and print the result
response = chain.run()
print("\n=== LLM Response ===\n")
print(response)
if __name__ == "__main__": if __name__ == "__main__":
+42
View File
@@ -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})`;
}
}
+79 -17
View File
@@ -1,21 +1,83 @@
from langchain_core.messages import HumanMessage, AIMessage
from typing import Dict, Any
def generate_response(state: Dict[str, Any]) -> Dict[str, Any]:
""" """
Simple node that echoes the user's message as an AI response. Node definitions for the graph.
Includes base Node, ReflectionNode, RewritingNode, InputNode, and OutputNode.
""" """
messages = state.get("messages", [])
if not messages:
return state
# Assume the last message is a HumanMessage from abc import ABC, abstractmethod
last_msg = messages[-1] from typing import Any, Dict
if isinstance(last_msg, HumanMessage):
# Create an AIMessage that echoes the content
ai_msg = AIMessage(content=f"Echo: {last_msg.content}")
messages.append(ai_msg)
# Update the state with the new messages list from .llm_integration import get_llm
state["messages"] = messages
return state
class BaseNode(ABC):
"""
Abstract base class for all nodes in the graph.
Each node must implement the `process` method.
"""
def __init__(self, node_id: str):
self.node_id = node_id
@abstractmethod
def process(self, input_data: Any) -> Any:
"""
Process the input data and return the output.
"""
pass
class InputNode(BaseNode):
"""
Node that simply passes through the input data.
"""
def process(self, input_data: Any) -> Any:
return input_data
class OutputNode(BaseNode):
"""
Node that collects the final output.
"""
def process(self, input_data: Any) -> Any:
return input_data
class ReflectionNode(BaseNode):
"""
Node that generates reflective insights from the input text using an LLM.
"""
def __init__(self, node_id: str, prompt_template: str = None):
super().__init__(node_id)
self.prompt_template = (
prompt_template
or "Please reflect on the following text:\n\n{input_text}\n\nReflection:"
)
self.llm = get_llm()
def process(self, input_data: str) -> Dict[str, str]:
prompt = self.prompt_template.format(input_text=input_data)
reflection = self.llm(prompt)
return {"reflection": reflection.strip()}
class RewritingNode(BaseNode):
"""
Node that rewrites the input text according to a specified style or instruction.
"""
def __init__(self, node_id: str, style: str = "formal"):
super().__init__(node_id)
self.style = style
self.llm = get_llm()
def process(self, input_data: Dict[str, str]) -> Dict[str, str]:
# Expecting input_data to contain 'reflection' key
reflection = input_data.get("reflection", "")
prompt = (
f"Rewrite the following reflection in a {self.style} style:\n\n{reflection}\n\nRewritten:"
)
rewritten = self.llm(prompt)
return {"rewritten": rewritten.strip()}
+35
View File
@@ -0,0 +1,35 @@
const { OpenAI } = require('langchain-openai');
const { ChatPromptTemplate, HumanMessagePromptTemplate } = require('langchain-core/prompts');
const { LLMChain } = require('langchain-core/chains');
// Initialize the LLM (OpenAI) with a moderate temperature for reflective responses
const llm = new OpenAI({ temperature: 0.7 });
// Prompt template for reflection
const prompt = ChatPromptTemplate.fromPromptMessages([
HumanMessagePromptTemplate.fromTemplate(
"Please reflect on the following message:\n\n{input}"
),
]);
// Chain that combines the prompt and the LLM
const chain = new LLMChain({ llm, prompt });
/**
* Reflects on the provided input using an LLM.
* @param {string} input - The message to reflect upon.
* @returns {Promise<string>} - The reflective output from the LLM.
*/
async function reflect(input) {
if (typeof input !== 'string') {
throw new Error('Reflect node expects a string input.');
}
try {
const result = await chain.invoke({ input });
return result.output;
} catch (err) {
throw new Error(`Reflect node error: ${err.message}`);
}
}
module.exports = { reflect };
+40
View File
@@ -0,0 +1,40 @@
"""
Reflect node for LangGraph.
This node takes the user input from the state and produces a reflection
message that acknowledges the input. The output is a dictionary containing
the key 'reflection'.
"""
from langgraph.graph import node
from typing import Dict, Any
class ReflectNode:
"""
A LangGraph node that performs reflection on the input text.
"""
@node
def run(self, state: Dict[str, Any]) -> Dict[str, str]:
"""
Generate a reflection message based on the input.
Parameters
----------
state : dict
The current state of the graph. Expected to contain an 'input'
key with the user-provided text.
Returns
-------
dict
A dictionary with a single key 'reflection' containing the
reflection message.
"""
input_text = state.get("input", "")
reflection = (
f"I see that you said: '{input_text}'. "
"Let's reflect on that."
)
return {"reflection": reflection}
+35
View File
@@ -0,0 +1,35 @@
const { OpenAI } = require('langchain-openai');
const { ChatPromptTemplate, HumanMessagePromptTemplate } = require('langchain-core/prompts');
const { LLMChain } = require('langchain-core/chains');
// Initialize the LLM (OpenAI) with a moderate temperature for rewriting
const llm = new OpenAI({ temperature: 0.7 });
// Prompt template for rewriting
const prompt = ChatPromptTemplate.fromPromptMessages([
HumanMessagePromptTemplate.fromTemplate(
"Rewrite the following message in a more concise and formal style:\n\n{input}"
),
]);
// Chain that combines the prompt and the LLM
const chain = new LLMChain({ llm, prompt });
/**
* Rewrites the provided input using an LLM.
* @param {string} input - The message to rewrite.
* @returns {Promise<string>} - The rewritten output from the LLM.
*/
async function rewrite(input) {
if (typeof input !== 'string') {
throw new Error('Rewrite node expects a string input.');
}
try {
const result = await chain.invoke({ input });
return result.output;
} catch (err) {
throw new Error(`Rewrite node error: ${err.message}`);
}
}
module.exports = { rewrite };
+38
View File
@@ -0,0 +1,38 @@
"""
Rewrite node for LangGraph.
This node takes the reflection produced by the ReflectNode and rewrites
it to a more formal style. The output is a dictionary containing
the key 'rewritten'.
"""
from langgraph.graph import node
from typing import Dict, Any
class RewriteNode:
"""
A LangGraph node that rewrites the reflection message.
"""
@node
def run(self, state: Dict[str, Any]) -> Dict[str, str]:
"""
Rewrite the reflection message.
Parameters
----------
state : dict
The current state of the graph. Expected to contain a 'reflection'
key with the message produced by the ReflectNode.
Returns
-------
dict
A dictionary with a single key 'rewritten' containing the
rewritten message.
"""
reflection = state.get("reflection", "")
# Simple rewrite: replace "I see" with "I notice"
rewritten = reflection.replace("I see", "I notice")
return {"rewritten": rewritten}
+88
View File
@@ -0,0 +1,88 @@
const { Graph } = require('../src');
describe('Graph', () => {
let graph;
beforeEach(() => {
graph = new Graph();
});
test('adds nodes correctly', () => {
graph.addNode('A', { value: 1 });
expect(graph.getNode('A')).toEqual({ value: 1 });
expect(() => graph.addNode('A')).toThrow(/already exists/);
});
test('adds edges correctly, including self-referential', () => {
graph.addNode('A');
graph.addNode('B');
const e1 = graph.addEdge('A', 'B', { weight: 5 });
const e2 = graph.addEdge('A', 'A', { weight: 3 }); // self-edge
expect(graph.getEdge(e1)).toEqual({ from: 'A', to: 'B', data: { weight: 5 } });
expect(graph.getEdge(e2)).toEqual({ from: 'A', to: 'A', data: { weight: 3 } });
expect(() => graph.addEdge('X', 'A')).toThrow(/does not exist/);
});
test('reflects an edge', () => {
graph.addNode('X');
graph.addNode('Y');
const e = graph.addEdge('X', 'Y', { relation: 'friend' });
const rev = graph.reflect(e);
expect(graph.getEdge(rev)).toEqual({ from: 'Y', to: 'X', data: { relation: 'friend' } });
});
test('refines a node', () => {
graph.addNode('N', { type: 'original' });
graph.addNode('M');
graph.addEdge('N', 'M', { link: true });
const refined = graph.refineNode('N', { type: 'refined' });
expect(refined).toBe('N_refined');
expect(graph.getNode(refined)).toEqual({ type: 'refined' });
// Original node still exists
expect(graph.getNode('N')).toEqual({ type: 'original' });
// Outgoing edge cloned
const outgoing = graph.getAdjacency(refined);
expect(outgoing.size).toBe(1);
const clonedEdgeId = Array.from(outgoing)[0];
const clonedEdge = graph.getEdge(clonedEdgeId);
expect(clonedEdge).toEqual({ from: refined, to: 'M', data: { link: true } });
});
test('refines an edge', () => {
graph.addNode('P');
graph.addNode('Q');
const e = graph.addEdge('P', 'Q', { cost: 10 });
const refined = graph.refineEdge(e, { cost: 20 });
expect(refined).toBe(`${e}_refined`);
expect(graph.getEdge(refined)).toEqual({ from: 'P', to: 'Q', data: { cost: 20 } });
// Original edge remains unchanged
expect(graph.getEdge(e)).toEqual({ from: 'P', to: 'Q', data: { cost: 10 } });
});
test('handles complex operations', () => {
graph.addNode('A');
graph.addNode('B');
graph.addNode('C');
const e1 = graph.addEdge('A', 'B', { weight: 1 });
const e2 = graph.addEdge('B', 'C', { weight: 2 });
const e3 = graph.addEdge('C', 'A', { weight: 3 });
// Reflect all edges
const rev1 = graph.reflect(e1);
const rev2 = graph.reflect(e2);
const rev3 = graph.reflect(e3);
// Refine node B
const refinedB = graph.refineNode('B', { status: 'active' });
// Verify adjacency of refined node
const adj = graph.getAdjacency(refinedB);
expect(adj.size).toBe(2); // edges to C and A (original outgoing edges)
});
});
+8
View File
@@ -0,0 +1,8 @@
const { createNode } = require('../../src/index');
test('Reflection node returns input unchanged', () => {
const node = createNode('Reflection');
const input = { a: 1 };
const output = node.execute(input);
expect(output).toBe(input);
});
+8
View File
@@ -0,0 +1,8 @@
const { createNode } = require('../../src/index');
test('Rewrite node replaces pattern', () => {
const node = createNode('Rewrite', { pattern: /foo/g, replacement: 'bar' });
const input = 'foo baz foo';
const output = node.execute(input);
expect(output).toBe('bar baz bar');
});
+67 -50
View File
@@ -1,64 +1,81 @@
const Graph = require('../src/graph'); import { Graph, Node, ReflectionNode, RewritingNode } from '../src/index.js';
describe('Graph', () => { describe('Graph with reflection and rewriting nodes', () => {
test('should add reflection node and evaluate correctly', () => { let graph;
const g = new Graph();
g.addNode('A', 'reflection'); beforeEach(() => {
const outputs = g.evaluate('A', 42); graph = new Graph();
expect(outputs['A']).toBe(42);
}); });
test('should add rewrite node and evaluate correctly', () => { test('can add generic, reflection, and rewriting nodes', () => {
const g = new Graph(); const n1 = new Node('n1');
g.addNode('B', 'rewrite'); const r1 = new ReflectionNode('r1');
const outputs = g.evaluate('B', 'hello'); const w1 = new RewritingNode('w1');
expect(outputs['B']).toBe('HELLO');
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', () => { test('adding duplicate node id throws error', () => {
const g = new Graph(); const n1 = new Node('dup');
g.addNode('A', 'reflection'); graph.addNode(n1);
g.addNode('B', 'rewrite'); expect(() => graph.addNode(new Node('dup'))).toThrow(/already exists/);
g.addEdge('A', 'B');
const outputs = g.evaluate('A', 'test');
expect(outputs['A']).toBe('test');
expect(outputs['B']).toBe('TEST');
}); });
test('should throw error on unknown node type', () => { test('can add edges between any node types', () => {
const g = new Graph(); const n1 = new Node('n1');
expect(() => g.addNode('C', 'unknown')).toThrow(); 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', () => { test('removeNode removes node and its edges', () => {
const g = new Graph(); const n1 = new Node('n1');
g.addNode('D', 'reflection'); const r1 = new ReflectionNode('r1');
expect(() => g.addNode('D', 'rewrite')).toThrow(); 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', () => { test('traverse handles disconnected graph', () => {
const g = new Graph(); const n1 = new Node('n1');
g.addNode('E', 'reflection'); const r1 = new ReflectionNode('r1');
expect(() => g.addEdge('E', 'F')).toThrow(); 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 visited = [];
const g = new Graph(); graph.traverse('n1', (node) => visited.push(node.id));
g.addNode('G', 'rewrite', { transform: (x) => x * 2 }); expect(visited).toEqual(['n1', 'r1']);
const outputs = g.evaluate('G', 5); // w1 is disconnected
expect(outputs['G']).toBe(10); expect(() => graph.traverse('w1', (node) => visited.push(node.id))).not.toThrow();
});
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');
}); });
}); });
+14
View File
@@ -0,0 +1,14 @@
import pytest
from src.graph import build_graph
def test_graph_flow():
graph = build_graph()
input_state = {"input": "Hello world"}
result = graph.invoke(input_state)
assert "rewritten" in result
expected = (
"I notice that you said: 'Hello world'. "
"Let's reflect on that."
)
assert result["rewritten"] == expected
+54
View File
@@ -0,0 +1,54 @@
"""
Unit tests for ReflectionNode and RewritingNode.
"""
import unittest
from unittest.mock import MagicMock, patch
from src.nodes import ReflectionNode, RewritingNode
class TestNodes(unittest.TestCase):
@patch("src.llm_integration.get_llm")
def test_reflection_node(self, mock_get_llm):
# Mock LLM to return a fixed reflection
mock_llm = MagicMock()
mock_llm.return_value = "This is a reflection."
mock_get_llm.return_value = mock_llm
node = ReflectionNode("test_reflection")
input_text = "Sample input text."
output = node.process(input_text)
self.assertIsInstance(output, dict)
self.assertIn("reflection", output)
self.assertEqual(output["reflection"], "This is a reflection.")
# Ensure LLM was called with correct prompt
expected_prompt = (
"Please reflect on the following text:\n\nSample input text.\n\nReflection:"
)
mock_llm.assert_called_once_with(expected_prompt)
@patch("src.llm_integration.get_llm")
def test_rewriting_node(self, mock_get_llm):
# Mock LLM to return a fixed rewritten text
mock_llm = MagicMock()
mock_llm.return_value = "Rewritten text."
mock_get_llm.return_value = mock_llm
node = RewritingNode("test_rewriting", style="formal")
input_data = {"reflection": "This is a reflection."}
output = node.process(input_data)
self.assertIsInstance(output, dict)
self.assertIn("rewritten", output)
self.assertEqual(output["rewritten"], "Rewritten text.")
# Ensure LLM was called with correct prompt
expected_prompt = (
"Rewrite the following reflection in a formal style:\n\nThis is a reflection.\n\nRewritten:"
)
mock_llm.assert_called_once_with(expected_prompt)
if __name__ == "__main__":
unittest.main()
+13
View File
@@ -0,0 +1,13 @@
import pytest
from src.nodes.reflect import ReflectNode
def test_reflect_node():
state = {"input": "Hello world"}
result = ReflectNode.run(state)
assert "reflection" in result
expected = (
"I see that you said: 'Hello world'. "
"Let's reflect on that."
)
assert result["reflection"] == expected
+18
View File
@@ -0,0 +1,18 @@
import pytest
from src.nodes.rewrite import RewriteNode
def test_rewrite_node():
state = {
"reflection": (
"I see that you said: 'Hello world'. "
"Let's reflect on that."
)
}
result = RewriteNode.run(state)
assert "rewritten" in result
expected = (
"I notice that you said: 'Hello world'. "
"Let's reflect on that."
)
assert result["rewritten"] == expected