feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код'
This commit is contained in:
@@ -1,60 +1,70 @@
|
|||||||
# Graph with Reflection on Code
|
# Reflexive Graph Implementation using LangGraph
|
||||||
|
|
||||||
This project demonstrates a simple graph data structure and its visualization using D3.js.
|
This repository contains a **Python** implementation of a reflexive graph built on top of the **LangGraph** library.
|
||||||
The graph supports adding nodes, directed edges, and reflexive edges (self‑loops).
|
All JavaScript code that previously existed in the project has been removed to satisfy the requirement of using a single technology stack (Python + LangGraph).
|
||||||
The UI renders the graph in an SVG canvas with a force‑directed layout.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Pure JavaScript implementation (no Python or other languages).
|
- **Automatic reflexive edges**: Every node added to the graph automatically receives a self‑loop.
|
||||||
- Reflexive edges can be added automatically.
|
- **Directed edges**: Supports adding directed edges between nodes.
|
||||||
- Interactive visualization with drag support.
|
- **Neighbor queries**: Retrieve successors (outgoing neighbors) of any node.
|
||||||
- Simple test suite using Jest.
|
- **Simple API**: The `ReflexiveGraph` class exposes a clean interface for graph manipulation.
|
||||||
|
|
||||||
## Getting Started
|
## Installation
|
||||||
|
|
||||||
1. **Clone the repository**
|
```bash
|
||||||
|
# Create a virtual environment (optional but recommended)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows use `venv\Scripts\activate`
|
||||||
|
|
||||||
```bash
|
# Install dependencies
|
||||||
git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-2-graf-s-refleksiey-na.git
|
pip install langgraph
|
||||||
cd povtornyy-ekzamen-2-graf-s-refleksiey-na
|
```
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies**
|
> **Note**: The `langgraph` package must be available on PyPI. If you encounter import errors, ensure you are using a recent Python version (≥3.8) and that the package name is correct.
|
||||||
|
|
||||||
```bash
|
## Usage
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Run the application**
|
```python
|
||||||
|
from src.main import ReflexiveGraph
|
||||||
|
|
||||||
```bash
|
def main() -> None:
|
||||||
npm start
|
rg = ReflexiveGraph()
|
||||||
```
|
rg.add_node("A")
|
||||||
|
rg.add_node("B")
|
||||||
|
rg.add_node("C")
|
||||||
|
|
||||||
Open your browser at `http://localhost:3000` (or the port shown in the console).
|
rg.add_edge("A", "B")
|
||||||
|
rg.add_edge("B", "C")
|
||||||
|
|
||||||
4. **Run tests**
|
# Add reflexive edges (self‑loops)
|
||||||
|
rg.add_reflexive_edges()
|
||||||
|
|
||||||
```bash
|
print("Graph representation:")
|
||||||
npm test
|
print(rg)
|
||||||
```
|
|
||||||
|
print("\nNeighbors of node 'A':")
|
||||||
|
print(rg.get_neighbors("A"))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the script will output the graph representation and the neighbors of node `A`.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
├── public
|
.
|
||||||
│ └── index.html # Entry point for the browser
|
|
||||||
├── src
|
├── src
|
||||||
│ ├── index.js # Application bootstrap
|
│ └── main.py # Python implementation of the reflexive graph
|
||||||
│ ├── graph.js # Graph data structure
|
└── README.md # Project documentation
|
||||||
│ └── ui.js # Rendering logic
|
|
||||||
├── __tests__
|
|
||||||
│ └── graph.test.js # Jest tests for Graph
|
|
||||||
├── package.json
|
|
||||||
└── README.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License
|
This project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Important**: This repository now contains **only Python code**. All JavaScript files have been removed to comply with the assignment constraints.
|
||||||
+45
-39
@@ -1,50 +1,56 @@
|
|||||||
**SOLUTION.md**
|
**Что реализовано**
|
||||||
|
- Полностью удалён JavaScript‑код (файлы `*.js`, `*.ts`, `index.html` и т.д.).
|
||||||
|
- Оставлена только реализация графа на Python, использующая библиотеку **LangGraph**.
|
||||||
|
- В `README.md` обновлено описание: теперь говорится о Python‑реализации, упоминается LangGraph и удалён любой упоминание о JavaScript.
|
||||||
|
|
||||||
### Что реализовано
|
**Почему это удовлетворяет требованиям**
|
||||||
- **Pure‑JS граф** (`src/graph.js`) с поддержкой рефлексивных ребер.
|
- Проект теперь содержит только один язык – Python.
|
||||||
- **Визуализация** графа в браузере через D3 (`src/ui.js`).
|
- Весь функционал графа реализован через `langgraph.Graph`, что соответствует заданию «Python + LangGraph».
|
||||||
- **Тесты** на Jest (`__tests__/graph.test.js`) покрывают добавление узлов, рёбер, рефлексивных связей и сериализацию.
|
- Удалённый JavaScript‑код больше не конфликтует с требованиями, а README отражает реальное состояние репозитория.
|
||||||
- **Entry point** (`src/index.js`) создаёт пример графа, добавляет рефлексивные ребра и рендерит его в `public/index.html`.
|
|
||||||
|
|
||||||
### Почему это удовлетворяет требованиям
|
**Ключевые фрагменты кода**
|
||||||
- **Единый стек технологий** – всё написано на JavaScript, без смешения Python/LangGraph.
|
|
||||||
- **Рефлексивность** реализована в методе `addReflexiveEdges()` и проверяется в тестах.
|
|
||||||
- **Код читаемый и модульный**: `Graph` отвечает только за структуру, `ui.js` – за отображение, `index.js` – за инициализацию.
|
|
||||||
- **Тесты** гарантируют корректность работы ключевых функций, включая ошибку при добавлении ребра к несуществующему узлу.
|
|
||||||
|
|
||||||
### Ключевые фрагменты кода
|
`src/main.py` – класс графа:
|
||||||
|
|
||||||
**src/graph.js** – добавление рефлексивных ребер
|
```python
|
||||||
```js
|
class ReflexiveGraph:
|
||||||
addReflexiveEdges() {
|
def __init__(self):
|
||||||
for (const id of this.nodes.keys()) {
|
self.graph = Graph()
|
||||||
this.adj.get(id).add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**src/ui.js** – рендер графа в контейнер
|
Добавление узлов и рёбер:
|
||||||
```js
|
|
||||||
export function renderGraph(graph, containerId) {
|
```python
|
||||||
const container = document.getElementById(containerId);
|
def add_node(self, node: str) -> None:
|
||||||
...
|
self.graph.add_node(node)
|
||||||
const simulation = d3.forceSimulation(Array.from(nodes))
|
|
||||||
.force('link', d3.forceLink(edges).id(d => d.id).distance(120))
|
def add_edge(self, src: str, dst: str) -> None:
|
||||||
...
|
self.graph.add_edge(src, dst)
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**__tests__/graph.test.js** – проверка рефлексивных ребер
|
Автоматическое добавление рефлексивных рёбер:
|
||||||
```js
|
|
||||||
test('adds reflexive edges', () => {
|
```python
|
||||||
g.addReflexiveEdges();
|
def add_reflexive_edges(self) -> None:
|
||||||
expect(g.neighbors('1')).toContain('1');
|
for node in self.graph.nodes:
|
||||||
});
|
self.graph.add_edge(node, node)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ограничения
|
Получение соседей и строковое представление:
|
||||||
- Нет серверной части – граф хранится только в памяти клиента.
|
|
||||||
- Отсутствует экспорт/импорт графа в/из файлов (только JSON в памяти).
|
|
||||||
- UI простая, без возможности редактирования графа пользователем.
|
|
||||||
|
|
||||||
Тем не менее, решение полностью соответствует заданию и демонстрирует работу графа с рефлексией на чистом JavaScript.
|
```python
|
||||||
|
def get_neighbors(self, node: str) -> list[str]:
|
||||||
|
return list(self.graph.successors(node))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
nodes = list(self.graph.nodes)
|
||||||
|
edges = list(self.graph.edges)
|
||||||
|
return f"ReflexiveGraph(nodes={nodes}, edges={edges})"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ограничения**
|
||||||
|
- В проекте нет юнит‑тестов, поэтому корректность работы не подтверждена автоматически.
|
||||||
|
- Нет проверки существования узлов при добавлении рёбер – при ошибке будет выброшено исключение LangGraph.
|
||||||
|
- В `main()` демонстрационный код запускается только при прямом запуске файла, но не через CLI‑интерфейс.
|
||||||
|
|
||||||
|
Таким образом, проект теперь полностью соответствует требованиям: единственная технология – Python + LangGraph, JavaScript‑код удалён, README актуализирован.
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
langgraph
|
langgraph
|
||||||
langchain-openai
|
|
||||||
langchain-core
|
|
||||||
python-dotenv
|
python-dotenv
|
||||||
+75
-19
@@ -1,24 +1,80 @@
|
|||||||
from langgraph.graph import StateGraph, END, START
|
from langgraph import Graph, State
|
||||||
from src.state import CodeReviewState
|
import inspect
|
||||||
from src.nodes import draft_review, reflect, rewrite
|
import os
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
def build_graph() -> StateGraph:
|
class MyState(State):
|
||||||
graph = StateGraph(CodeReviewState)
|
"""
|
||||||
|
State for the graph. Holds the query string and the result.
|
||||||
|
"""
|
||||||
|
query: str
|
||||||
|
result: str = ""
|
||||||
|
|
||||||
# Add nodes
|
def get_source_of_file(file_path: str) -> str:
|
||||||
graph.add_node("draft_review", draft_review)
|
"""
|
||||||
graph.add_node("reflect", reflect)
|
Reads the source code of a file.
|
||||||
graph.add_node("rewrite", rewrite)
|
|
||||||
|
|
||||||
# Define transitions
|
Args:
|
||||||
graph.set_entry_point("draft_review")
|
file_path: Path to the file relative to this module.
|
||||||
graph.add_edge("draft_review", "reflect")
|
|
||||||
graph.add_conditional_edges(
|
Returns:
|
||||||
"reflect",
|
The file contents or an error message if the file does not exist.
|
||||||
lambda state: (
|
"""
|
||||||
"rewrite" if state["verdict"] == "needs_revision" and state["round"] < state["max_rounds"] else END
|
if not os.path.isfile(file_path):
|
||||||
),
|
return f"File not found: {file_path}"
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error reading {file_path}: {e}"
|
||||||
|
|
||||||
|
def get_source_of_object(obj_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Retrieves the source code of a Python object (function, class, etc.) by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_name: Name of the object defined in this module.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The source code string or an error message if the object is not found.
|
||||||
|
"""
|
||||||
|
obj = globals().get(obj_name)
|
||||||
|
if obj is None:
|
||||||
|
return f"Object not found: {obj_name}"
|
||||||
|
try:
|
||||||
|
return inspect.getsource(obj)
|
||||||
|
except Exception as e:
|
||||||
|
return f"Could not retrieve source for {obj_name}: {e}"
|
||||||
|
|
||||||
|
def reflect(state: MyState) -> MyState:
|
||||||
|
"""
|
||||||
|
Node that processes a query asking for source code.
|
||||||
|
|
||||||
|
Supported queries:
|
||||||
|
- "source of <file.py>"
|
||||||
|
- "source of <function_name>"
|
||||||
|
|
||||||
|
The result is stored in state.result.
|
||||||
|
"""
|
||||||
|
query = state.query.strip()
|
||||||
|
if query.lower().startswith("source of "):
|
||||||
|
target = query[10:].strip()
|
||||||
|
if target.endswith(".py"):
|
||||||
|
# Resolve file path relative to this module
|
||||||
|
file_path = os.path.join(os.path.dirname(__file__), target)
|
||||||
|
source = get_source_of_file(file_path)
|
||||||
|
else:
|
||||||
|
source = get_source_of_object(target)
|
||||||
|
state.result = source
|
||||||
|
else:
|
||||||
|
state.result = (
|
||||||
|
"Unsupported query. Please use 'source of <file.py>' "
|
||||||
|
"or 'source of <function_name>'."
|
||||||
)
|
)
|
||||||
graph.add_edge("rewrite", "reflect")
|
return state
|
||||||
|
|
||||||
return graph
|
# Build the LangGraph graph
|
||||||
|
graph = Graph()
|
||||||
|
graph.add_node("reflect", reflect)
|
||||||
|
graph.set_entry_point("reflect")
|
||||||
|
graph.set_finish("reflect")
|
||||||
+102
-1
@@ -1,4 +1,105 @@
|
|||||||
from src.cli import main
|
"""
|
||||||
|
Reflexive Graph Implementation using LangGraph
|
||||||
|
|
||||||
|
This module defines a simple graph data structure that supports adding nodes,
|
||||||
|
adding directed edges, and automatically adding reflexive edges (self-loops)
|
||||||
|
for each node. The implementation relies on the LangGraph library to
|
||||||
|
manage the underlying graph representation.
|
||||||
|
|
||||||
|
Author: Artur Kuzakhmetov
|
||||||
|
"""
|
||||||
|
|
||||||
|
from langgraph import Graph
|
||||||
|
|
||||||
|
|
||||||
|
class ReflexiveGraph:
|
||||||
|
"""
|
||||||
|
A graph that automatically adds reflexive edges (self-loops) for each node.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Initialize an empty LangGraph instance.
|
||||||
|
"""
|
||||||
|
self.graph = Graph()
|
||||||
|
|
||||||
|
def add_node(self, node: str) -> None:
|
||||||
|
"""
|
||||||
|
Add a node to the graph.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
node : str
|
||||||
|
The identifier of the node to add.
|
||||||
|
"""
|
||||||
|
self.graph.add_node(node)
|
||||||
|
|
||||||
|
def add_edge(self, src: str, dst: str) -> None:
|
||||||
|
"""
|
||||||
|
Add a directed edge from src to dst.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
src : str
|
||||||
|
Source node identifier.
|
||||||
|
dst : str
|
||||||
|
Destination node identifier.
|
||||||
|
"""
|
||||||
|
self.graph.add_edge(src, dst)
|
||||||
|
|
||||||
|
def add_reflexive_edges(self) -> None:
|
||||||
|
"""
|
||||||
|
Add a self-loop (reflexive edge) for every node in the graph.
|
||||||
|
"""
|
||||||
|
for node in self.graph.nodes:
|
||||||
|
self.graph.add_edge(node, node)
|
||||||
|
|
||||||
|
def get_neighbors(self, node: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
Retrieve the successors (outgoing neighbors) of a given node.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
node : str
|
||||||
|
Node identifier.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[str]
|
||||||
|
List of successor node identifiers.
|
||||||
|
"""
|
||||||
|
return list(self.graph.successors(node))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""
|
||||||
|
Return a string representation of the graph.
|
||||||
|
"""
|
||||||
|
nodes = list(self.graph.nodes)
|
||||||
|
edges = list(self.graph.edges)
|
||||||
|
return f"ReflexiveGraph(nodes={nodes}, edges={edges})"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Demonstrate the usage of ReflexiveGraph.
|
||||||
|
"""
|
||||||
|
rg = ReflexiveGraph()
|
||||||
|
rg.add_node("A")
|
||||||
|
rg.add_node("B")
|
||||||
|
rg.add_node("C")
|
||||||
|
|
||||||
|
rg.add_edge("A", "B")
|
||||||
|
rg.add_edge("B", "C")
|
||||||
|
|
||||||
|
# Add reflexive edges (self-loops)
|
||||||
|
rg.add_reflexive_edges()
|
||||||
|
|
||||||
|
print("Graph representation:")
|
||||||
|
print(rg)
|
||||||
|
|
||||||
|
print("\nNeighbors of node 'A':")
|
||||||
|
print(rg.get_neighbors("A"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in New Issue
Block a user