Skip to content

Commit 9dafb69

Browse files
authored
Merge pull request #2 from viartemev/orchestrator
RAG implementation
2 parents 238230d + 8aebb4b commit 9dafb69

32 files changed

Lines changed: 945 additions & 523 deletions

README.md

Lines changed: 78 additions & 88 deletions
Large diffs are not rendered by default.

app/agents/agent.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@
55
import os
66
import json
77
import logging
8+
import re
89
from openai import OpenAI, APIError
910
from openai.types.chat import ChatCompletionMessage
1011
from dotenv import load_dotenv
1112
import inspect
1213

14+
from app.rag.retriever import KnowledgeRetriever
15+
from app.agents.tools import (
16+
read_file_tool, read_file_tool_def,
17+
list_files_tool, list_files_tool_def,
18+
)
19+
1320
# Новый, улучшенный системный промпт, превращающий агента в программиста.
1421
SYSTEM_PROMPT = """
1522
Ты — автономный AI-агент, способный выполнять сложные задачи, используя доступные инструменты.
@@ -55,6 +62,8 @@ def __init__(
5562
api_key: str,
5663
model: str = "o4-mini",
5764
max_iterations: int = 10,
65+
use_rag: bool = False,
66+
rag_config: Optional[Dict[str, Any]] = None,
5867
):
5968
self.name = name
6069
self.role = role
@@ -67,6 +76,25 @@ def __init__(
6776
self.max_iterations = max_iterations
6877
self.system_prompt = "Ты — универсальный AI-ассистент."
6978

79+
# Add default tools
80+
self.add_tool(read_file_tool, read_file_tool_def)
81+
self.add_tool(list_files_tool, list_files_tool_def)
82+
83+
# RAG specific attributes
84+
self.use_rag = use_rag
85+
self.rag_config = rag_config or {}
86+
self.retriever: Optional[KnowledgeRetriever] = None
87+
if self.use_rag:
88+
try:
89+
self.retriever = KnowledgeRetriever()
90+
except FileNotFoundError as e:
91+
logging.warning(
92+
f"Agent '{self.name}' was configured to use RAG, "
93+
f"but the knowledge base is not found. RAG will be disabled. "
94+
f"Error: {e}"
95+
)
96+
self.use_rag = False
97+
7098
def add_tool(self, tool_func: Callable, tool_definition: Dict[str, Any]):
7199
"""Добавляет инструмент и его определение."""
72100
tool_name = tool_func.__name__
@@ -79,6 +107,58 @@ def get_openai_tools(self) -> Optional[List[Dict[str, Any]]]:
79107
return None
80108
return self.tool_definitions
81109

110+
def _create_rag_query(self, briefing: str) -> str:
111+
"""
112+
Creates a focused query for RAG from the detailed briefing.
113+
Extracts the current task and the last result from history.
114+
"""
115+
task_match = re.search(r"\*\*YOUR CURRENT TASK \(Step .*\):\*\*\n\n\*\*Task:\*\* (.*)\n\*\*Description:\*\* (.*)", briefing)
116+
history_match = re.search(r"\*\*EXECUTION HISTORY:\*\*\n(.*)", briefing, re.DOTALL)
117+
118+
if not task_match:
119+
return briefing # Fallback to full briefing
120+
121+
task = task_match.group(1)
122+
description = task_match.group(2)
123+
124+
query_parts = [f"Task: {task}", f"Description: {description}"]
125+
126+
if history_match:
127+
history_str = history_match.group(1).strip()
128+
# Get the last entry from the history
129+
last_entry = history_str.split("- **Step")[0].strip()
130+
if last_entry:
131+
query_parts.append(f"Context from previous step: {last_entry}")
132+
133+
focused_query = "\n".join(query_parts)
134+
logging.info(f"Created focused RAG query for {self.name}: '{focused_query}'")
135+
return focused_query
136+
137+
def _enrich_with_knowledge(self, query: str) -> str:
138+
"""Enriches a query with context from the knowledge base if RAG is enabled."""
139+
if not self.use_rag or not self.retriever:
140+
return ""
141+
142+
top_k = self.rag_config.get("top_k", 3)
143+
filters = self.rag_config.get("filters", None)
144+
145+
logging.info(f"Agent '{self.name}' is retrieving knowledge with top_k={top_k}, filters={filters}")
146+
retrieved_knowledge = self.retriever.retrieve(query=query, top_k=top_k, filters=filters)
147+
148+
if not retrieved_knowledge:
149+
logging.info("No specific internal standards found for this query.")
150+
return ""
151+
152+
formatted_knowledge = "\n\n---\n\n".join(
153+
[f"Source: {chunk['source']}\n\n{chunk['text']}" for chunk in retrieved_knowledge]
154+
)
155+
knowledge_context = (
156+
"Before you begin, consult these internal standards and best practices:"
157+
f"\n\n--- RELEVANT KNOWLEDGE ---\n{formatted_knowledge}\n--------------------------\n"
158+
)
159+
logging.info(f"Knowledge context added for agent '{self.name}'.")
160+
return knowledge_context
161+
82162
def _execute_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
83163
"""Выполняет указанный инструмент с аргументами."""
84164
if tool_name in self.tools:
@@ -126,9 +206,17 @@ def execute_task(self, briefing: str) -> str:
126206
"""
127207
logging.info(f"Агент {self.name} получил задачу.")
128208

209+
knowledge_context = ""
210+
if self.use_rag:
211+
focused_query = self._create_rag_query(briefing)
212+
knowledge_context = self._enrich_with_knowledge(query=focused_query)
213+
129214
# Системный промпт определяет "личность" агента, а брифинг - контекст задачи.
215+
# Контекст из базы знаний добавляется в начало системного промпта.
216+
final_system_prompt = f"{knowledge_context}\n{self.system_prompt}"
217+
130218
self.conversation_history = [
131-
{"role": "system", "content": self.system_prompt},
219+
{"role": "system", "content": final_system_prompt},
132220
{"role": "user", "content": briefing}
133221
]
134222

app/agents/roles/coding_agent.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,59 @@
11
"""Coding Agent."""
22
from typing import List, Dict
33
from app.agents.agent import Agent
4+
from app.agents.tools import (
5+
read_file_tool, read_file_tool_def,
6+
edit_file_tool, edit_file_tool_def,
7+
list_files_tool, list_files_tool_def,
8+
)
49

510

611
class CodingAgent(Agent):
712
"""An agent specializing in writing and refactoring code."""
813

9-
def __init__(self, **kwargs):
10-
super().__init__(**kwargs)
14+
def __init__(self, name: str, role: str, goal: str, **kwargs):
15+
super().__init__(
16+
name=name,
17+
role=role,
18+
goal=goal,
19+
**kwargs,
20+
)
21+
# Self-register tools
22+
self.add_tool(read_file_tool, read_file_tool_def)
23+
self.add_tool(edit_file_tool, edit_file_tool_def)
24+
self.add_tool(list_files_tool, list_files_tool_def)
25+
1126
self.system_prompt = (
1227
"You are a CodingAgent, an elite AI developer. Your task is to write, modify, and fix code."
1328
"You will be provided with the full plan, the history of previous steps, and your current task."
1429
"\n\n"
1530
"## Operating Principles:\n"
1631
"1. **Think Before You Code:** Carefully study the task and context. Plan your actions."
17-
"2. **Follow Instructions:** Precisely follow the given task, whether it's writing a new function, fixing a bug, or refactoring."
18-
"3. **Use Tools Wisely:** Do not call tools unnecessarily. Analyze first, then act."
32+
"2. **Surgical Edits:** Your primary goal is to make precise, targeted changes. Do not rewrite entire files. Instead, identify the specific function, method, or block of code that needs changing and replace only that part.\n"
33+
"3. **Use Tools Wisely:**\n"
34+
" - Prefer `edit_file_tool` with `mode='replace'`. This is the safest and most professional way to work.\n"
35+
" - Use `mode='append'` for adding new functions or tests to the end of a file.\n"
36+
" - Use `mode='overwrite'` ONLY when creating a brand new file.\n"
1937
"4. **Code Quality:** Write clean, efficient, and well-documented code that adheres to PEP8."
20-
"5. **Handling Code Review Feedback:** "
21-
" - Carefully review ALL feedback from the ReviewerAgent."
22-
" - **'Read-Modify-Overwrite' Strategy:** Instead of many small fixes, use the following approach:"
23-
" a. Read the file's content (`read_file_tool`)."
24-
" b. Apply ALL necessary changes in memory."
25-
" c. Completely overwrite the file with a single call to `edit_file_tool` using `mode='overwrite'` and the full new content."
26-
" - This approach ensures that all corrections are applied atomically and nothing is missed."
2738
"\n\n"
39+
"## Example of a Surgical Edit:\n"
40+
"Your task is to fix a bug in the `add` function in `math_utils.py`.\n\n"
41+
"1. **First, read the file:** `read_file_tool(path='app/utils/math_utils.py')`\n"
42+
"2. **Identify the flawed function:**\n"
43+
" ```python\n"
44+
" # This is the old, incorrect code block you will replace\n"
45+
" def add(a, b):\n"
46+
" return a - b # Bug is here\n"
47+
" ```\n"
48+
"3. **Call the edit tool to replace ONLY that function:**\n"
49+
" ```python\n"
50+
" edit_file_tool(\n"
51+
" path='app/utils/math_utils.py',\n"
52+
" mode='replace',\n"
53+
" old_content='''def add(a, b):\\n return a - b # Bug is here''',\n"
54+
" new_content='''def add(a, b):\\n # Fix: Correctly perform addition\\n return a + b'''\n"
55+
" )\n"
56+
" ```\n\n"
2857
"Your goal is to successfully complete your part of the plan, preparing the way for the next agent."
2958
)
3059

app/agents/roles/evaluator_agent.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
"""Evaluator Agent."""
22
from app.agents.agent import Agent
3+
from app.agents.tools import (
4+
read_file_tool, read_file_tool_def,
5+
)
36

47
class EvaluatorAgent(Agent):
58
"""An agent that analyzes errors and results."""
6-
def __init__(self, **kwargs):
7-
super().__init__(**kwargs)
9+
def __init__(self, name: str, role: str, goal: str, **kwargs):
10+
super().__init__(
11+
name=name,
12+
role=role,
13+
goal=goal,
14+
**kwargs,
15+
)
16+
# Self-register tools
17+
self.add_tool(read_file_tool, read_file_tool_def)
18+
819
self.system_prompt = """
920
You are EvaluatorAgent, an experienced QA engineer and systems analyst.
1021
Your primary task is to analyze the log from failed pytest runs and formulate a clear, concise, and single task for the CodingAgent to fix the code.

app/agents/roles/reviewer_agent.py

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,25 @@
11
"""Reviewer Agent."""
22
from app.agents.agent import Agent
3-
from app.rag.retriever import KnowledgeRetriever
3+
from app.agents.tools import (
4+
read_file_tool, read_file_tool_def,
5+
)
46

57
class ReviewerAgent(Agent):
68
"""An agent specializing in strict Code Review."""
7-
def __init__(self, name: str = "CodeReviewer", **kwargs):
9+
def __init__(self, name: str, role: str, goal: str, **kwargs):
810
super().__init__(
911
name=name,
10-
role="Code Reviewer",
11-
goal=(
12-
"Ensure that the provided code is of high quality, "
13-
"free of errors, and adheres to best practices and "
14-
"internal coding standards."
15-
),
12+
role=role,
13+
goal=goal,
1614
**kwargs,
1715
)
18-
self.retriever = KnowledgeRetriever()
16+
# Self-register tools
17+
self.add_tool(read_file_tool, read_file_tool_def)
1918

20-
def _get_system_prompt(self, **kwargs) -> str:
21-
code_to_review = kwargs.get("code", "")
22-
23-
# Retrieve relevant knowledge
24-
retrieved_knowledge = self.retriever.retrieve(query=code_to_review, top_k=3)
25-
26-
knowledge_context = "No specific internal standards found for this code."
27-
if retrieved_knowledge:
28-
formatted_knowledge = "\n\n---\n\n".join(
29-
[f"Source: {chunk['source']}\n\n{chunk['text']}" for chunk in retrieved_knowledge]
30-
)
31-
knowledge_context = (
32-
"When performing the review, pay close attention to the following "
33-
"internal standards and best practices:\n\n"
34-
f"--- RELEVANT KNOWLEDGE ---\n{formatted_knowledge}\n--------------------------"
35-
)
36-
37-
return (
38-
f"You are a Senior Software Engineer acting as a code reviewer. "
39-
f"Your task is to provide a thorough review of the given code snippet.\n\n"
40-
f"{knowledge_context}\n\n"
41-
f"Please review the following code:\n\n"
42-
f"```python\n{code_to_review}\n```\n\n"
43-
f"Provide your feedback in a clear, constructive manner. "
44-
f"If you find issues, suggest specific improvements."
19+
self.system_prompt = (
20+
"You are a Senior Software Engineer acting as a code reviewer. "
21+
"Your task is to provide a thorough review of the code based on the "
22+
"provided file path. Use your available tools to read the file content.\n\n"
23+
"If the code meets all standards, respond with only the word 'LGTM'.\n"
24+
"Otherwise, provide clear, constructive feedback on what needs to be improved."
4525
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Task Decomposer Agent."""
2+
from app.agents.agent import Agent
3+
import logging
4+
import json
5+
6+
class TaskDecomposer(Agent):
7+
"""
8+
An agent specializing in breaking down a high-level goal into a step-by-step plan.
9+
"""
10+
def __init__(self, name: str, role: str, goal: str, **kwargs):
11+
# This agent typically does not need RAG, but the option is there.
12+
kwargs.setdefault('use_rag', False)
13+
14+
super().__init__(
15+
name=name,
16+
role=role,
17+
goal=goal,
18+
**kwargs,
19+
)
20+
self.system_prompt = """
21+
You are an expert project manager. Your task is to break down a high-level user goal into a concise, step-by-step plan.
22+
Each step must be a single, clear action assigned to one of the available roles.
23+
24+
# AVAILABLE ROLES:
25+
- CodingAgent: Writes, modifies, and fixes code.
26+
- TestingAgent: Runs tests and reports results.
27+
- ReviewerAgent: Performs code reviews, checking for quality and adherence to standards.
28+
- EvaluatorAgent: Analyzes test failures and creates bug reports.
29+
30+
# OUTPUT FORMAT:
31+
Your output must be a list of steps in JSON format. Do not include any other text or explanation.
32+
33+
# EXAMPLE:
34+
Goal: "Create a function to add two numbers and test it."
35+
36+
Your output:
37+
```json
38+
[
39+
{
40+
"step": 1,
41+
"assignee": "CodingAgent",
42+
"task": "Create a new function 'add(a, b)' in 'app/utils/math.py'."
43+
},
44+
{
45+
"step": 2,
46+
"assignee": "ReviewerAgent",
47+
"task": "Review the 'add' function in 'app/utils/math.py'."
48+
},
49+
{
50+
"step": 3,
51+
"assignee": "CodingAgent",
52+
"task": "Create a new test file 'tests/test_math.py' with tests for the 'add' function."
53+
},
54+
{
55+
"step": 4,
56+
"assignee": "TestingAgent",
57+
"task": "Run the tests in 'tests/test_math.py'."
58+
}
59+
]
60+
```
61+
"""
62+
63+
def get_plan(self, goal: str) -> list:
64+
"""
65+
Generates a plan for a given goal.
66+
Overrides the base 'execute_task' to return a structured plan.
67+
"""
68+
task_briefing = f"Create a step-by-step plan to achieve the following goal: {goal}"
69+
70+
try:
71+
logging.info("Requesting plan from OpenAI...")
72+
response = self.client.chat.completions.create(
73+
model=self.model,
74+
messages=[
75+
{"role": "system", "content": self.system_prompt},
76+
{"role": "user", "content": task_briefing},
77+
],
78+
response_format={"type": "json_object"},
79+
)
80+
81+
response_content = response.choices[0].message.content
82+
logging.info("Received raw plan: %s", response_content)
83+
84+
# The response is a JSON string, so we need to parse it.
85+
plan = json.loads(response_content)
86+
# Sometimes the model returns a dictionary with a "plan" key
87+
if isinstance(plan, dict) and "plan" in plan:
88+
return plan["plan"]
89+
return plan
90+
91+
except json.JSONDecodeError as e:
92+
logging.error(f"Failed to decode JSON from OpenAI response: {e}")
93+
logging.error(f"Raw response was: {response_content}")
94+
return [{"step": 1, "assignee": "DefaultAgent", "task": "Failed to create a valid plan due to JSON error."}]
95+
except Exception as e:
96+
logging.error(f"An unexpected error occurred while getting the plan: {e}", exc_info=True)
97+
return [{"step": 1, "assignee": "DefaultAgent", "task": "Failed to create a plan due to an unexpected error."}]

0 commit comments

Comments
 (0)