197 lines
5.8 KiB
Python
197 lines
5.8 KiB
Python
#!/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() |