feat: solution for 'Экзамен: Самокорректирующийся агент'

This commit is contained in:
2026-06-30 16:48:01 +03:00
parent 2bd56fb1bc
commit 3f1fe15e38
4 changed files with 227 additions and 190 deletions
+66 -15
View File
@@ -1,26 +1,77 @@
# SelfCorrecting Agent
# Assignment: Самокорректирующийся агент
This repository demonstrates a minimal setup for a selfcorrecting agent using **langgraph** and **langchainopenai**.
The project includes:
This repository contains a small commandline 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 Python3.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 machinereadable
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 wellformed 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
* Python3.9 or newer
* No thirdparty packages
## License
This project is released under the MIT License. Feel free to use and
modify it as you wish.
+1 -2
View File
@@ -1,2 +1 @@
langchain-openai
langgraph
# No external dependencies required
+91 -173
View File
@@ -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 selfcorrecting 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 userprovided 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] = [
"Главная",
"Мои задания",
"Экзамен: Самокорректирующийся агент",
"",
"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 selfcorrecting 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("SelfCorrecting 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 commandline 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()
+69
View File
@@ -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()