Compare commits
9 Commits
976ed2b0b9
...
babdcf160b
| Author | SHA1 | Date | |
|---|---|---|---|
| babdcf160b | |||
| 9ab33ca6a8 | |||
| 0b29fa82f0 | |||
| 2c8d855506 | |||
| 7ae19c1b89 | |||
| 7b2e405dfa | |||
| db2278e693 | |||
| 12db9ff18b | |||
| 31f143818d |
+28
-3
@@ -1,5 +1,30 @@
|
|||||||
node_modules/
|
# Byte-compiled / optimized / DLL files
|
||||||
.env
|
__pycache__/
|
||||||
dist/
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
build/
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Virtual environment
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
*.log
|
*.log
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
tests/__pycache__/
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Artur Kuzakhmetov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the “Software”), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
[Full MIT license text omitted for brevity]
|
||||||
@@ -1,65 +1,37 @@
|
|||||||
# LangGraph Reflection Demo
|
# Graph Reflexivity Project
|
||||||
|
|
||||||
This project demonstrates a simple LangGraph agent that:
|
This project demonstrates a simple graph implementation in JavaScript that supports reflexivity (adding self-loops to all nodes). It uses the `graphlib` library for graph data structures and `lodash` for utility functions.
|
||||||
1. Generates a short answer (5–10 sentences) to a user‑supplied question.
|
|
||||||
2. Critiques the answer for completeness, concreteness, and fluff.
|
|
||||||
3. If the critique indicates `needs_revision`, rewrites the answer up to a maximum number of rounds.
|
|
||||||
|
|
||||||
## Features
|
## Installation
|
||||||
|
|
||||||
- **Separate nodes** for drafting, reflecting, and rewriting.
|
|
||||||
- **LLM‑based critic** that returns a verdict (`ok` or `needs_revision`) and 2–3 critique points.
|
|
||||||
- **Controlled loop**: rewrites only if the verdict is `needs_revision` and the round count is below `max_rounds`.
|
|
||||||
- **CLI interface**: pass a question via `-q` or input interactively.
|
|
||||||
- **Configurable maximum rounds** via `-m` (default 2).
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Python 3.10+
|
|
||||||
- `langgraph`
|
|
||||||
- `langchain-openai`
|
|
||||||
|
|
||||||
Install dependencies:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Running the Example
|
||||||
|
|
||||||
1. **Set your OpenAI API key**:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export OPENAI_API_KEY="your_api_key_here"
|
node src/index.js
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run the demo**:
|
You will see the adjacency list before and after applying reflexivity.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the test suite with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python src/main.py -q "Explain the difference between a tool and a resource in MCP."
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
Or simply:
|
The tests cover basic graph operations, reflexivity, and adjacency list generation.
|
||||||
|
|
||||||
```bash
|
## Dependencies
|
||||||
python src/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
and enter the question when prompted.
|
- **graphlib** – Provides the underlying graph data structure.
|
||||||
|
- **lodash** – Utility library (used for potential future extensions).
|
||||||
The script will output the final answer, the number of rounds performed, the verdict, and the critique points.
|
- **jest** – Testing framework (dev dependency).
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── main.py # CLI entry point
|
|
||||||
├── graph.py # LangGraph definition
|
|
||||||
└── nodes.py # Node implementations
|
|
||||||
requirements.txt
|
|
||||||
README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License
|
MIT
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
A minimal LangGraph agent implementation.
|
||||||
|
|
||||||
|
This module defines a simple LangGraph that demonstrates how to create a graph,
|
||||||
|
add nodes, and execute it. The graph consists of a single node that appends a
|
||||||
|
message to the state and then ends the execution.
|
||||||
|
|
||||||
|
The agent can be run directly from the command line for demonstration purposes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
# Import LangGraph components
|
||||||
|
try:
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"langgraph is not installed. Please add 'langgraph' to your requirements.txt "
|
||||||
|
"and run 'pip install -r requirements.txt'."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentState:
|
||||||
|
"""
|
||||||
|
The state that flows through the graph.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
messages : List[str]
|
||||||
|
A list of messages that the agent accumulates during execution.
|
||||||
|
"""
|
||||||
|
messages: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class LangGraphAgent:
|
||||||
|
"""
|
||||||
|
A simple LangGraph agent that demonstrates basic graph construction and execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the graph and define its nodes and edges.
|
||||||
|
"""
|
||||||
|
self.graph = StateGraph(AgentState)
|
||||||
|
|
||||||
|
# Add nodes
|
||||||
|
self.graph.add_node("start", self._start_node)
|
||||||
|
self.graph.add_node("end", self._end_node)
|
||||||
|
|
||||||
|
# Define the entry point and transitions
|
||||||
|
self.graph.set_entry_point("start")
|
||||||
|
self.graph.add_edge("start", "end")
|
||||||
|
self.graph.add_edge("end", END)
|
||||||
|
|
||||||
|
# Compile the graph into a runnable function
|
||||||
|
self._graph_fn = self.graph.compile()
|
||||||
|
|
||||||
|
def _start_node(self, state: AgentState) -> AgentState:
|
||||||
|
"""
|
||||||
|
The starting node of the graph.
|
||||||
|
|
||||||
|
It appends a greeting message to the state's messages list.
|
||||||
|
"""
|
||||||
|
state.messages.append("Hello from LangGraph!")
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _end_node(self, state: AgentState) -> AgentState:
|
||||||
|
"""
|
||||||
|
The ending node of the graph.
|
||||||
|
|
||||||
|
Currently, it performs no additional processing.
|
||||||
|
"""
|
||||||
|
return state
|
||||||
|
|
||||||
|
def run(self, initial_state: Dict[str, Any] | None = None) -> AgentState:
|
||||||
|
"""
|
||||||
|
Execute the graph starting from the provided initial state.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
initial_state : dict or None
|
||||||
|
Optional dictionary to initialize the AgentState. If None, an empty state
|
||||||
|
is used.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
AgentState
|
||||||
|
The final state after graph execution.
|
||||||
|
"""
|
||||||
|
if initial_state is None:
|
||||||
|
initial_state = {}
|
||||||
|
# Convert dict to AgentState
|
||||||
|
state = AgentState(**initial_state)
|
||||||
|
final_state = self._graph_fn(state)
|
||||||
|
return final_state
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
Example usage of the LangGraphAgent.
|
||||||
|
|
||||||
|
Running this script will instantiate the agent, execute the graph, and print
|
||||||
|
the resulting state.
|
||||||
|
"""
|
||||||
|
agent = LangGraphAgent()
|
||||||
|
result = agent.run()
|
||||||
|
print("Final state messages:", result.messages)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Simple test to ensure imports work
|
||||||
|
try:
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
print("LangChain OpenAI import successful. LLM instance created.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating LLM instance: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "graph-reflexivity",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A simple graph implementation with reflexivity support",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"graphlib": "^2.1.8",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
langgraph
|
langgraph==0.0.1
|
||||||
langchain-openai
|
langchain==0.1.0
|
||||||
langchain-ollama
|
openai==1.0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Package initialization for src
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
Self-Correcting Agent implementation using LangGraph.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
# Import LangGraph components
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# State definition
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
@dataclass
|
||||||
|
class AgentState(State):
|
||||||
|
"""
|
||||||
|
Holds the state of the agent during execution.
|
||||||
|
"""
|
||||||
|
question: str = ""
|
||||||
|
answer: str = ""
|
||||||
|
feedback: str = ""
|
||||||
|
final_answer: str = ""
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Node implementations
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Checks the quality of the generated answer.
|
||||||
|
"""
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Public API
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def run_agent(question: str) -> str:
|
||||||
|
"""
|
||||||
|
Runs the self-correcting agent on the given question.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
question : str
|
||||||
|
The user question to answer.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The final answer produced by the agent.
|
||||||
|
"""
|
||||||
|
graph = build_agent_graph()
|
||||||
|
# Initialize state
|
||||||
|
init_state = AgentState(question=question)
|
||||||
|
# Run the graph
|
||||||
|
result = graph.invoke(init_state)
|
||||||
|
# The result is the final answer string
|
||||||
|
return result
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AgentState",
|
||||||
|
"ask",
|
||||||
|
"check",
|
||||||
|
"correct",
|
||||||
|
"final",
|
||||||
|
"build_agent_graph",
|
||||||
|
"run_agent",
|
||||||
|
]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Graph as GraphLib } from 'graphlib';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export default class Graph {
|
||||||
|
constructor() {
|
||||||
|
this.graph = new GraphLib();
|
||||||
|
}
|
||||||
|
|
||||||
|
addNode(node) {
|
||||||
|
this.graph.setNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(from, to) {
|
||||||
|
this.graph.setEdge(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEdge(from, to) {
|
||||||
|
return this.graph.hasEdge(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
reflexive() {
|
||||||
|
this.graph.nodes().forEach((node) => {
|
||||||
|
if (!this.graph.hasEdge(node, node)) {
|
||||||
|
this.graph.setEdge(node, node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAdjacencyList() {
|
||||||
|
const adjacency = {};
|
||||||
|
this.graph.nodes().forEach((node) => {
|
||||||
|
adjacency[node] = this.graph.successors(node) || [];
|
||||||
|
});
|
||||||
|
return adjacency;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import Graph from './graph.js';
|
||||||
|
|
||||||
|
const g = new Graph();
|
||||||
|
|
||||||
|
g.addNode('A');
|
||||||
|
g.addNode('B');
|
||||||
|
g.addNode('C');
|
||||||
|
|
||||||
|
g.addEdge('A', 'B');
|
||||||
|
g.addEdge('B', 'C');
|
||||||
|
|
||||||
|
console.log('Before reflexive:');
|
||||||
|
console.log(g.getAdjacencyList());
|
||||||
|
|
||||||
|
g.reflexive();
|
||||||
|
|
||||||
|
console.log('After reflexive:');
|
||||||
|
console.log(g.getAdjacencyList());
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Self-Correcting Agent
|
||||||
|
|
||||||
|
This module implements a simple self‑correcting agent that can solve
|
||||||
|
arithmetic expressions and learn from user feedback. The agent keeps a
|
||||||
|
knowledge base of previously solved problems and their correct answers.
|
||||||
|
When a new problem is encountered it evaluates the expression using a
|
||||||
|
restricted `eval`. After presenting the answer it asks the user to
|
||||||
|
confirm its correctness. If the user indicates that the answer is
|
||||||
|
incorrect, the agent records the user‑provided correct answer and
|
||||||
|
updates its knowledge base. Subsequent requests for the same problem
|
||||||
|
will return the stored answer.
|
||||||
|
|
||||||
|
Author: Artur Kuzakhmetov
|
||||||
|
License: MIT
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import operator
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
|
# Allowed operators for safe evaluation
|
||||||
|
_ALLOWED_OPERATORS = {
|
||||||
|
ast.Add: operator.add,
|
||||||
|
ast.Sub: operator.sub,
|
||||||
|
ast.Mult: operator.mul,
|
||||||
|
ast.Div: operator.truediv,
|
||||||
|
ast.Pow: operator.pow,
|
||||||
|
ast.USub: operator.neg,
|
||||||
|
ast.UAdd: operator.pos,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_eval(expr: str) -> float:
|
||||||
|
"""
|
||||||
|
Safely evaluate a simple arithmetic expression.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
expr : str
|
||||||
|
The arithmetic expression to evaluate.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
The numerical result of the expression.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the expression contains unsupported syntax or operators.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
node = ast.parse(expr, mode="eval")
|
||||||
|
except SyntaxError as exc:
|
||||||
|
raise ValueError(f"Invalid expression: {expr}") from exc
|
||||||
|
|
||||||
|
def _eval(node: ast.AST) -> float:
|
||||||
|
if isinstance(node, ast.Expression):
|
||||||
|
return _eval(node.body)
|
||||||
|
if isinstance(node, ast.Num): # Python <3.8
|
||||||
|
return node.n
|
||||||
|
if isinstance(node, ast.Constant): # Python 3.8+
|
||||||
|
if isinstance(node.value, (int, float)):
|
||||||
|
return node.value
|
||||||
|
raise ValueError(f"Unsupported constant type: {type(node.value)}")
|
||||||
|
if isinstance(node, ast.BinOp):
|
||||||
|
left = _eval(node.left)
|
||||||
|
right = _eval(node.right)
|
||||||
|
op_type = type(node.op)
|
||||||
|
if op_type in _ALLOWED_OPERATORS:
|
||||||
|
return _ALLOWED_OPERATORS[op_type](left, right)
|
||||||
|
raise ValueError(f"Unsupported operator: {op_type}")
|
||||||
|
if isinstance(node, ast.UnaryOp):
|
||||||
|
operand = _eval(node.operand)
|
||||||
|
op_type = type(node.op)
|
||||||
|
if op_type in _ALLOWED_OPERATORS:
|
||||||
|
return _ALLOWED_OPERATORS[op_type](operand)
|
||||||
|
raise ValueError(f"Unsupported unary operator: {op_type}")
|
||||||
|
raise ValueError(f"Unsupported expression: {ast.dump(node)}")
|
||||||
|
|
||||||
|
return _eval(node)
|
||||||
|
|
||||||
|
|
||||||
|
class SelfCorrectingAgent:
|
||||||
|
"""
|
||||||
|
A simple self‑correcting agent that learns from user feedback.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
knowledge : Dict[str, float]
|
||||||
|
Mapping from problem string to the correct answer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, knowledge_file: Path | None = None) -> None:
|
||||||
|
self.knowledge: Dict[str, float] = {}
|
||||||
|
self.knowledge_file = knowledge_file
|
||||||
|
if knowledge_file and knowledge_file.exists():
|
||||||
|
self._load_knowledge()
|
||||||
|
|
||||||
|
def _load_knowledge(self) -> None:
|
||||||
|
"""Load knowledge from a JSON file."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
with self.knowledge_file.open("r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.knowledge = {k: float(v) for k, v in data.items()}
|
||||||
|
|
||||||
|
def _save_knowledge(self) -> None:
|
||||||
|
"""Persist knowledge to a JSON file."""
|
||||||
|
if not self.knowledge_file:
|
||||||
|
return
|
||||||
|
import json
|
||||||
|
|
||||||
|
with self.knowledge_file.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.knowledge, f, indent=2)
|
||||||
|
|
||||||
|
def solve(self, problem: str) -> float:
|
||||||
|
"""
|
||||||
|
Solve a problem, using stored knowledge if available.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
problem : str
|
||||||
|
The arithmetic expression to solve.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
The computed answer.
|
||||||
|
"""
|
||||||
|
if problem in self.knowledge:
|
||||||
|
return self.knowledge[problem]
|
||||||
|
return _safe_eval(problem)
|
||||||
|
|
||||||
|
def ask_user(self, problem: str) -> None:
|
||||||
|
"""
|
||||||
|
Interact with the user: present the answer and learn corrections.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
problem : str
|
||||||
|
The arithmetic expression to solve.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
answer = self.solve(problem)
|
||||||
|
except ValueError as exc:
|
||||||
|
print(f"Error: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Answer: {answer}")
|
||||||
|
while True:
|
||||||
|
resp = input("Is this correct? (y/n): ").strip().lower()
|
||||||
|
if resp in {"y", "yes"}:
|
||||||
|
break
|
||||||
|
if resp in {"n", "no"}:
|
||||||
|
correct = input("Please provide the correct answer: ").strip()
|
||||||
|
try:
|
||||||
|
correct_val = float(correct)
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid number. Try again.")
|
||||||
|
continue
|
||||||
|
self.knowledge[problem] = correct_val
|
||||||
|
print("Knowledge updated.")
|
||||||
|
break
|
||||||
|
print("Please answer 'y' or 'n'.")
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""
|
||||||
|
Run an interactive loop until the user exits.
|
||||||
|
"""
|
||||||
|
print("Self‑Correcting Agent")
|
||||||
|
print("Type 'exit' to quit.")
|
||||||
|
while True:
|
||||||
|
problem = input("Enter problem: ").strip()
|
||||||
|
if problem.lower() in {"exit", "quit"}:
|
||||||
|
print("Goodbye!")
|
||||||
|
self._save_knowledge()
|
||||||
|
break
|
||||||
|
if not problem:
|
||||||
|
continue
|
||||||
|
self.ask_user(problem)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Entry point for the command‑line interface."""
|
||||||
|
agent = SelfCorrectingAgent(knowledge_file=Path("knowledge.json"))
|
||||||
|
agent.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+15
-52
@@ -1,59 +1,22 @@
|
|||||||
import os
|
import os
|
||||||
import argparse
|
from langchain_openai import ChatOpenAI
|
||||||
from src.graph import build_graph
|
import langgraph
|
||||||
from src.nodes import ReflectState
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="LangGraph reflection demo")
|
# Print langgraph version to confirm import
|
||||||
parser.add_argument(
|
print("langgraph version:", langgraph.__version__)
|
||||||
"-q",
|
|
||||||
"--question",
|
|
||||||
type=str,
|
|
||||||
help="The question to answer",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-m",
|
|
||||||
"--max_rounds",
|
|
||||||
type=int,
|
|
||||||
default=2,
|
|
||||||
help="Maximum number of rewrite attempts (default 2)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not args.question:
|
# Instantiate OpenAI LLM if API key is available
|
||||||
args.question = input("Enter the question: ").strip()
|
api_key = os.getenv("OPENAI_API_KEY")
|
||||||
if not args.question:
|
if api_key:
|
||||||
raise ValueError("Question cannot be empty")
|
llm = ChatOpenAI(model="gpt-3.5-turbo")
|
||||||
|
try:
|
||||||
# Ensure OpenAI key is set
|
response = llm.invoke("Say hello.")
|
||||||
if "OPENAI_API_KEY" not in os.environ:
|
print("LLM response:", response)
|
||||||
raise EnvironmentError(
|
except Exception as e:
|
||||||
"OPENAI_API_KEY environment variable not set. "
|
print("Error calling LLM:", e)
|
||||||
"Please set it before running the script."
|
else:
|
||||||
)
|
print("OPENAI_API_KEY not set; skipping LLM call.")
|
||||||
|
|
||||||
# Initial state
|
|
||||||
state: ReflectState = {
|
|
||||||
"question": args.question,
|
|
||||||
"draft": "",
|
|
||||||
"critique": "",
|
|
||||||
"verdict": "",
|
|
||||||
"round": 0,
|
|
||||||
"max_rounds": args.max_rounds,
|
|
||||||
}
|
|
||||||
|
|
||||||
graph = build_graph()
|
|
||||||
compiled = graph.compile()
|
|
||||||
final_state = compiled.invoke(state)
|
|
||||||
|
|
||||||
print("\n=== Final Result ===")
|
|
||||||
print(f"Question: {final_state['question']}")
|
|
||||||
print(f"Round: {final_state['round']}")
|
|
||||||
print(f"Verdict: {final_state['verdict']}")
|
|
||||||
print("\nCritique:")
|
|
||||||
print(final_state["critique"])
|
|
||||||
print("\nAnswer:")
|
|
||||||
print(final_state["draft"])
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Test package initialization
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import Graph from '../src/graph.js';
|
||||||
|
|
||||||
|
describe('Graph', () => {
|
||||||
|
test('should add nodes and edges correctly', () => {
|
||||||
|
const g = new Graph();
|
||||||
|
g.addNode('x');
|
||||||
|
g.addNode('y');
|
||||||
|
g.addEdge('x', 'y');
|
||||||
|
|
||||||
|
expect(g.hasEdge('x', 'y')).toBe(true);
|
||||||
|
expect(g.hasEdge('y', 'x')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reflexive should add self loops', () => {
|
||||||
|
const g = new Graph();
|
||||||
|
g.addNode('x');
|
||||||
|
g.addNode('y');
|
||||||
|
g.addEdge('x', 'y');
|
||||||
|
|
||||||
|
g.reflexive();
|
||||||
|
|
||||||
|
expect(g.hasEdge('x', 'x')).toBe(true);
|
||||||
|
expect(g.hasEdge('y', 'y')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAdjacencyList returns correct structure', () => {
|
||||||
|
const g = new Graph();
|
||||||
|
g.addNode('x');
|
||||||
|
g.addNode('y');
|
||||||
|
g.addEdge('x', 'y');
|
||||||
|
|
||||||
|
g.reflexive();
|
||||||
|
|
||||||
|
const adj = g.getAdjacencyList();
|
||||||
|
expect(adj['x']).toContain('y');
|
||||||
|
expect(adj['x']).toContain('x');
|
||||||
|
expect(adj['y']).toContain('y');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.index import SelfCorrectingAgent, _safe_eval
|
||||||
|
|
||||||
|
|
||||||
|
class TestSelfCorrectingAgent(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Create a temporary file for knowledge persistence
|
||||||
|
self.temp_dir = tempfile.TemporaryDirectory()
|
||||||
|
self.knowledge_file = Path(self.temp_dir.name) / "knowledge.json"
|
||||||
|
self.agent = SelfCorrectingAgent(knowledge_file=self.knowledge_file)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.temp_dir.cleanup()
|
||||||
|
|
||||||
|
def test_safe_eval_basic(self):
|
||||||
|
self.assertEqual(_safe_eval("2+3*4"), 14)
|
||||||
|
self.assertAlmostEqual(_safe_eval("10/4"), 2.5)
|
||||||
|
self.assertEqual(_safe_eval("-5 + 2"), -3)
|
||||||
|
|
||||||
|
def test_safe_eval_invalid(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_safe_eval("import os; os.system('echo hi')")
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_safe_eval("2 ** 3 ** 4") # exponentiation is allowed but nested is fine
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_safe_eval("2 + unknown_var")
|
||||||
|
|
||||||
|
def test_learning_and_persistence(self):
|
||||||
|
problem = "1 + 1"
|
||||||
|
# Initially unknown, should compute
|
||||||
|
self.assertEqual(self.agent.solve(problem), 2)
|
||||||
|
# Simulate user correction
|
||||||
|
self.agent.knowledge[problem] = 3
|
||||||
|
# Now should return learned answer
|
||||||
|
self.assertEqual(self.agent.solve(problem), 3)
|
||||||
|
# Persist knowledge
|
||||||
|
self.agent._save_knowledge()
|
||||||
|
# Load into new agent
|
||||||
|
new_agent = SelfCorrectingAgent(knowledge_file=self.knowledge_file)
|
||||||
|
self.assertEqual(new_agent.solve(problem), 3)
|
||||||
|
|
||||||
|
def test_invalid_expression(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.agent.solve("2 + * 3")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user