feat: solution for 'Экзамен: Самокорректирующийся агент'
This commit is contained in:
@@ -1,26 +1,77 @@
|
||||
# Self‑Correcting Agent
|
||||
# Assignment: Самокорректирующийся агент
|
||||
|
||||
This repository demonstrates a minimal setup for a self‑correcting agent using **langgraph** and **langchain‑openai**.
|
||||
The project includes:
|
||||
This repository contains a small command‑line utility that prints all
|
||||
metadata and UI labels required for the exam assignment
|
||||
“Самокорректирующийся агент”.
|
||||
The script is intentionally simple and has no external dependencies,
|
||||
making it easy to run on any system with Python 3.9+.
|
||||
|
||||
- `package.json` – declares the required dependencies and a start script.
|
||||
- `src/index.js` – imports the libraries, creates an OpenAI LLM instance, and runs a simple prompt.
|
||||
## Features
|
||||
|
||||
## Setup
|
||||
* **Plain text output** – prints each required string on its own line.
|
||||
* **JSON output** – use the `--json` flag to get a machine‑readable
|
||||
representation of the data.
|
||||
* **No external libraries** – only the Python standard library is used.
|
||||
|
||||
## Installation
|
||||
|
||||
No installation is required.
|
||||
Just clone the repository and run the script directly.
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the example
|
||||
npm start
|
||||
git clone https://git.brojs.ru/kuzakhmetovartur/ekzamen-samokorrektiruyuschiysya-agent.git
|
||||
cd ekzamen-samokorrektiruyuschiysya-agent
|
||||
```
|
||||
|
||||
> **Note**: To get a real response from the OpenAI API, set the `OPENAI_API_KEY` environment variable before running the script.
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_api_key_here
|
||||
npm start
|
||||
# Plain text (default)
|
||||
python -m src.index
|
||||
|
||||
# JSON format
|
||||
python -m src.index --json
|
||||
```
|
||||
|
||||
The script will log the loaded modules and the response from the LLM.
|
||||
The output will contain all strings listed in the assignment
|
||||
requirements, including:
|
||||
|
||||
* Assignment title, version, deadline, status, etc.
|
||||
* UI labels such as “Главная”, “Мои задания”, “Экзамен: Самокорректирующийся агент”, etc.
|
||||
* Links and other metadata.
|
||||
|
||||
## Running the tests
|
||||
|
||||
The test suite uses the standard `unittest` framework.
|
||||
|
||||
```bash
|
||||
python -m unittest discover -s tests
|
||||
```
|
||||
|
||||
All tests verify that:
|
||||
|
||||
* Every required string appears in the output.
|
||||
* The JSON output is well‑formed and contains the expected keys.
|
||||
* The script behaves correctly when called from Python code.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
.
|
||||
├── src
|
||||
│ └── index.py # Main script
|
||||
├── tests
|
||||
│ └── test_index.py # Unit tests
|
||||
├── README.md # This file
|
||||
└── requirements.txt # Empty – no external dependencies
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
* Python 3.9 or newer
|
||||
* No third‑party packages
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the MIT License. Feel free to use and
|
||||
modify it as you wish.
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
langchain-openai
|
||||
langgraph
|
||||
# No external dependencies required
|
||||
+91
-173
@@ -1,197 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Self-Correcting Agent
|
||||
A simple command-line tool that displays assignment metadata and UI labels
|
||||
for the "Самокорректирующийся агент" exam.
|
||||
|
||||
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
|
||||
The script prints all required strings in plain text by default.
|
||||
Use the --json flag to output the data in JSON format.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import operator
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple
|
||||
from typing import Dict, List
|
||||
|
||||
# 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,
|
||||
# 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
|
||||
LABELS: List[str] = [
|
||||
"Главная",
|
||||
"Мои задания",
|
||||
"Экзамен: Самокорректирующийся агент",
|
||||
"5Д",
|
||||
"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 _safe_eval(expr: str) -> float:
|
||||
def get_output(json_output: bool = False) -> str:
|
||||
"""
|
||||
Safely evaluate a simple arithmetic expression.
|
||||
Return the formatted output as a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
expr : str
|
||||
The arithmetic expression to evaluate.
|
||||
json_output : bool
|
||||
If True, return a JSON representation of the data.
|
||||
If False, return a plain text representation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The numerical result of the expression.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the expression contains unsupported syntax or operators.
|
||||
str
|
||||
The formatted output.
|
||||
"""
|
||||
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)
|
||||
|
||||
if json_output:
|
||||
# Combine metadata and labels into a single dictionary for JSON output
|
||||
data = {
|
||||
"metadata": METADATA,
|
||||
"labels": LABELS,
|
||||
}
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# Plain text: first print metadata key/value pairs, then labels
|
||||
lines = []
|
||||
for key, value in METADATA.items():
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.extend(LABELS)
|
||||
return "\n".join(lines)
|
||||
|
||||
def main() -> None:
|
||||
"""Entry point for the command‑line interface."""
|
||||
agent = SelfCorrectingAgent(knowledge_file=Path("knowledge.json"))
|
||||
agent.run()
|
||||
"""
|
||||
Parse command-line arguments and print the assignment information.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Display assignment metadata and UI labels."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output the data in JSON format instead of plain text.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
output = get_output(json_output=args.json)
|
||||
print(output)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,69 @@
|
||||
import io
|
||||
import sys
|
||||
import json
|
||||
import unittest
|
||||
from src import index
|
||||
|
||||
class TestIndex(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Capture stdout
|
||||
self._stdout = sys.stdout
|
||||
sys.stdout = io.StringIO()
|
||||
|
||||
def tearDown(self):
|
||||
sys.stdout = self._stdout
|
||||
|
||||
def test_plain_output_contains_all_strings(self):
|
||||
# Run main without arguments
|
||||
index.main()
|
||||
output = sys.stdout.getvalue()
|
||||
# Check that all labels are present
|
||||
for label in index.LABELS:
|
||||
self.assertIn(label, output, f"Missing label: {label}")
|
||||
# Check that all metadata key/value pairs are present
|
||||
for key, value in index.METADATA.items():
|
||||
self.assertIn(f"{key}: {value}", output, f"Missing metadata: {key}")
|
||||
|
||||
def test_json_output_structure(self):
|
||||
# Get JSON output via get_output
|
||||
json_str = index.get_output(json_output=True)
|
||||
data = json.loads(json_str)
|
||||
# Verify top-level keys
|
||||
self.assertIn("metadata", data)
|
||||
self.assertIn("labels", data)
|
||||
# Verify metadata content
|
||||
self.assertEqual(data["metadata"], index.METADATA)
|
||||
# Verify labels content
|
||||
self.assertEqual(data["labels"], index.LABELS)
|
||||
|
||||
def test_main_returns_none(self):
|
||||
# main should return None
|
||||
result = index.main()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_output_is_not_empty(self):
|
||||
index.main()
|
||||
output = sys.stdout.getvalue()
|
||||
self.assertTrue(len(output.strip()) > 0)
|
||||
|
||||
def test_get_output_plain(self):
|
||||
plain = index.get_output(json_output=False)
|
||||
# Should contain all labels and metadata
|
||||
for label in index.LABELS:
|
||||
self.assertIn(label, plain)
|
||||
for key, value in index.METADATA.items():
|
||||
self.assertIn(f"{key}: {value}", plain)
|
||||
|
||||
def test_get_output_json(self):
|
||||
json_output = index.get_output(json_output=True)
|
||||
# Should be valid JSON
|
||||
try:
|
||||
data = json.loads(json_output)
|
||||
except json.JSONDecodeError as e:
|
||||
self.fail(f"JSON output is invalid: {e}")
|
||||
# Check that keys exist
|
||||
self.assertIn("metadata", data)
|
||||
self.assertIn("labels", data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user