diff --git a/.gitignore b/.gitignore index b16538b..d29d31c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,30 @@ -node_modules/ -.env -dist/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging build/ +dist/ +*.egg-info/ + +# Virtual environment +.venv/ +env/ +ENV/ +venv/ +ENV/ + +# Temporary files +*.tmp *.log +*.swp + +# IDE files +.vscode/ +.idea/ +*.sublime-workspace +*.sublime-project + +# Test artifacts +tests/__pycache__/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0963399 --- /dev/null +++ b/LICENSE @@ -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] \ No newline at end of file diff --git a/README.md b/README.md index faea470..b99cbed 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,79 @@ # Self‑Correcting Agent -This repository contains a minimal **Node.js** implementation of a self‑correcting agent. -The project uses **no external frameworks** – only the Node.js standard library. +A lightweight Python program that demonstrates a simple self‑correcting agent. +The agent evaluates arithmetic expressions, presents the result to the user, +and learns from user feedback. Once a problem has been corrected, the +agent remembers the correct answer and returns it automatically on +subsequent requests. + +> **Note** +> This project is intentionally minimal to illustrate the concept of a +> self‑correcting system. It is not intended for production use. ## Features -- **Whitespace normalization** – removes leading/trailing spaces and collapses multiple spaces. -- **Basic spelling correction** – a small dictionary of common misspellings is applied. -- **Punctuation handling** – ensures the sentence ends with a period, exclamation mark, or question mark. - -## Requirements - -- Node.js 14 or newer +- **Safe evaluation** of arithmetic expressions (`+`, `-`, `*`, `-`, `**`). +- **Interactive CLI**: type expressions, receive answers, and confirm correctness. +- **Learning**: when the user indicates an error, the agent stores the + correct answer and uses it in the future. +- **Persistence**: learned knowledge is saved to `knowledge.json` in the + current working directory. ## Installation -No installation is required. Just clone the repository and run the script. +The project requires Python 3.8 or newer. ```bash +# Clone the repository git clone https://git.brojs.ru/kuzakhmetovartur/ekzamen-samokorrektiruyuschiysya-agent.git cd ekzamen-samokorrektiruyuschiysya-agent + +# (Optional) Create a virtual environment +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\\Scripts\\activate + +# Install dependencies (none required for the core functionality) +pip install -r requirements.txt # Empty file, kept for compatibility ``` ## Usage -Run the script from the command line, passing the sentence you want to correct as an argument. +Run the program from the command line: ```bash -node src/index.js " This is teh example sentence wich needs correction " +python -m src.index ``` -Output: +You will see a prompt: ``` -This is the example sentence which needs correction. +Self‑Correcting Agent +Type 'exit' to quit. +Enter problem: ``` -## Project Structure +Enter an arithmetic expression, e.g.: ``` -src/ -└── index.js # Main implementation -README.md # Project documentation +Enter problem: 2 + 3 * 4 +```` + +The program will output: + +```` + +Answer: 14 +Is this correct? (y/n): +```` + +- **y** if the answer is correct. +- **n** and then provide the correct answer if the program made a mistake. + +To exit, type **exit** or **quit**. + +## Example Session + ``` - -## Contributing - -Feel free to fork the repository and submit pull requests. -All contributions should keep the dependency footprint minimal and use only the standard library. - -## License - -This project is licensed under the MIT License. \ No newline at end of file +Self‑Correcting Agent +Type … (truncated for brevity) +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3ceaffc..3f4f48a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -openai>=1.0.0 \ No newline at end of file +# No external dependencies required for the core functionality. +# This file is kept for compatibility with standard Python project layouts. \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..f501189 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +# Package initialization for src \ No newline at end of file diff --git a/src/index.py b/src/index.py index 96e0fac..0b62b63 100644 --- a/src/index.py +++ b/src/index.py @@ -1,67 +1,197 @@ #!/usr/bin/env python3 """ -Simple Self-Correcting Agent +Self-Correcting Agent -This script demonstrates a minimal self‑correcting agent that -takes a string input and attempts to correct common -typos such as extra spaces, missing punctuation, and -simple misspellings using a small dictionary. +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. -The implementation uses only the Python standard library -and does not depend on any external frameworks. +Author: Artur Kuzakhmetov +License: MIT """ -import sys -import re -from typing import List, Dict +from __future__ import annotations -# A very small dictionary of common misspellings -MISSPELLINGS: Dict[str, str] = { - "teh": "the", - "recieve": "receive", - "adress": "address", - "occured": "occurred", - "seperate": "separate", - "definately": "definitely", - "goverment": "government", - "untill": "until", - "accomodate": "accommodate", - "wich": "which", +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 correct_spelling(word: str) -> str: - """Return the corrected word if it is a known misspelling.""" - return MISSPELLINGS.get(word.lower(), word) -def correct_sentence(sentence: str) -> str: +def _safe_eval(expr: str) -> float: """ - Correct a sentence by: - 1. Removing leading/trailing whitespace. - 2. Collapsing multiple spaces into one. - 3. Correcting known misspellings. - 4. Ensuring the sentence ends with a period. + 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. """ - # Strip whitespace - sentence = sentence.strip() - # Collapse multiple spaces - sentence = re.sub(r"\s+", " ", sentence) - # Tokenise and correct words - words = sentence.split(" ") - corrected_words: List[str] = [correct_spelling(w) for w in words] - corrected = " ".join(corrected_words) - # Ensure ending punctuation - if not corrected.endswith((".", "!", "?")): - corrected += "." - return corrected + 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: - if len(sys.argv) < 2: - print("Usage: python -m src.index \"\"") - sys.exit(1) + """Entry point for the command‑line interface.""" + agent = SelfCorrectingAgent(knowledge_file=Path("knowledge.json")) + agent.run() - input_sentence = " ".join(sys.argv[1:]) - corrected = correct_sentence(input_sentence) - print(corrected) if __name__ == "__main__": main() \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..93a2d4b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Test package initialization \ No newline at end of file diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 0000000..f7ca9bd --- /dev/null +++ b/tests/test_agent.py @@ -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() \ No newline at end of file