Skip to content

Commit 664c639

Browse files
Merge pull request #76 from Grigorij-Dudnik/dev
Dev
2 parents b5e2de3 + 0bea506 commit 664c639

File tree

15 files changed

+293
-137
lines changed

15 files changed

+293
-137
lines changed

.github/readme.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div align="center">
22
<img src="/non_src/assets/starting_video.gif" alt="Demo">
33
<br>
4-
<img src="/non_src/assets/logo_wide_2.png" alt="Logo">
4+
<img src="/non_src/assets/logo_wide_3.jpg" alt="Logo">
55
<br>
66
<h2>Probably the most intelligent AI coder out there.</h2>
77
<br>
8-
Clean Coder is your 2-in-1 AI Scrum Master and Developer. Delegate planning, managing, and coding to AI. Agents create tasks within Todoist, write code, and test it, helping you create great projects with minimal effort!
8+
Clean Coder is your 2-in-1 AI Scrum Master and Developer. Equipped with most intelligent reasoning system and most andvanced codebase research tool, it makes your code written with absolute minimum time investment on your part!
99
<br>
1010
<br>
1111
<h3>⭐️ Your star motivates us to introduce new cool features! ⭐️</h3>
@@ -50,6 +50,12 @@ Create an entire web app ~~with~~ by Clean Coder:
5050

5151
## 📊 Why Clean Coder?
5252

53+
Our dream is to create a fully autonomous programmer one day, letting you relax (or at least do another job 😜) while your coding job will be done by AI.
54+
55+
That's why we care about making Clean Coder have top intelligence level and be equipped with most modern automation tools.
56+
57+
Learn more about Clean Coder's idea [here](https://clean-coder.dev/faq/why_clean_coder/).
58+
5359
| Feature | Clean Coder | Cline | Aider | Cursor |
5460
|---------|-------------|--------|-------|---------|
5561
| **Intelligence** | ✅ Two-step Planer agent for thinking only | 🟡 One-step plan mode | 🟡 One-step Architect agent | ❌ No thinking agent |
@@ -61,7 +67,7 @@ Create an entire web app ~~with~~ by Clean Coder:
6167

6268
## ✨ Key advantages:
6369

64-
- Get project supervised by [Manager agent](https://clean-coder.dev/usage/manager/) with thoroughly-described tasks organized in Todoist, just like with a human scrum master.
70+
- Get project supervised by [Manager agent](https://clean-coder.dev/usage/manager/) with tasks organized in Todoist, just like with a human scrum master.
6571
- Two-step planning module makes Clean Coder probably most intelligent [AI coder](https://clean-coder.dev/usage/programmer_pipeline/) available.
6672
- [Semantic search (RAG)](https://clean-coder.dev/advanced_features_installation/similarity_search_for_researcher/) for effective navigating even large codebases.
6773
- Allow AI to see frontend it creates with [frontend feedback feature](https://clean-coder.dev/features/frontend_feedback/). At the day of writing no other AI coder has that feature.

manager.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
add_todoist_envs()
1414

1515
from typing import TypedDict, Sequence
16-
from langchain_core.messages import BaseMessage, HumanMessage
16+
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
1717
from langchain_core.load import dumps
1818
from langgraph.graph import StateGraph
1919
from src.tools.tools_project_manager import add_task, modify_task, finish_project_planning, reorder_tasks
20-
from src.tools.tools_coder_pipeline import prepare_list_dir_tool, prepare_see_file_tool, ask_human_tool
20+
from src.tools.tools_coder_pipeline import prepare_list_dir_tool, prepare_see_file_tool, ask_human_tool, retrieve_files_by_semantic_query
2121
from src.tools.rag.index_file_descriptions import prompt_index_project_files
22+
from src.utilities.util_functions import save_state_history_to_disk, join_paths
2223
from src.utilities.manager_utils import (
2324
actualize_tasks_list_and_progress_description,
2425
setup_todoist_project_if_needed,
@@ -29,14 +30,14 @@
2930
call_tool,
3031
multiple_tools_msg,
3132
no_tools_msg,
32-
empty_message_msg,
3333
)
3434
from src.utilities.start_project_functions import set_up_dot_clean_coder_dir
35-
from src.utilities.util_functions import join_paths
3635
from src.utilities.llms import init_llms_medium_intelligence
3736
from src.utilities.print_formatters import print_formatted
37+
from src.tools.rag.retrieval import vdb_available
3838
import json
3939
import os
40+
import uuid
4041

4142

4243
class AgentState(TypedDict):
@@ -58,21 +59,34 @@ def __init__(self):
5859
self.saved_messages_path = join_paths(self.work_dir, ".clean_coder/manager_messages.json")
5960

6061
def call_model_manager(self, state):
61-
self.save_messages_to_disk(state)
62+
save_state_history_to_disk(state, self.saved_messages_path)
6263
state = call_model(state, self.llms)
6364
state = self.cut_off_context(state)
64-
state = call_tool(state, self.tools)
65-
messages = [msg for msg in state["messages"] if msg.type == "ai"]
66-
last_ai_message = messages[-1]
65+
66+
ai_messages = [msg for msg in state["messages"] if msg.type == "ai"]
67+
last_ai_message = ai_messages[-1]
68+
# in case model will return empty message (that strange thing happens for 3.5 and 3.7 sonnet after task execution),
69+
# we will replace it with tool call message to go with next task
6770
if not last_ai_message.content and not last_ai_message.tool_calls:
6871
state["messages"].pop()
69-
state["messages"].append(HumanMessage(content=empty_message_msg))
70-
elif len(last_ai_message.tool_calls) == 0:
72+
state["messages"].append(AIMessage(
73+
content="Proceeding with next task.",
74+
tool_calls=[
75+
{
76+
"name": "finish_project_planning",
77+
"args": {"dummy": "ok"},
78+
"id": str(uuid.uuid4()),
79+
"type": "tool_call",
80+
}
81+
]
82+
))
83+
state = call_tool(state, self.tools)
84+
if len(last_ai_message.tool_calls) == 0:
7185
state["messages"].append(HumanMessage(content=no_tools_msg))
72-
7386
state = actualize_tasks_list_and_progress_description(state)
7487
return state
7588

89+
7690
# Logic for conditional edges
7791
def after_agent_condition(self, state):
7892
last_message = state["messages"][-1]
@@ -116,11 +130,13 @@ def prepare_tools(self):
116130
add_task,
117131
modify_task,
118132
reorder_tasks,
119-
list_dir,
120-
see_file,
121133
ask_human_tool,
122134
finish_project_planning,
135+
list_dir,
136+
see_file,
123137
]
138+
if vdb_available():
139+
tools.append(retrieve_files_by_semantic_query)
124140
return tools
125141

126142
# workflow definition
@@ -133,7 +149,7 @@ def setup_workflow(self):
133149

134150
def run(self):
135151
print_formatted("😀 Hello! I'm Manager agent. Let's plan your project together!", color="green")
136-
152+
137153
messages = get_manager_messages(self.saved_messages_path)
138154
inputs = {"messages": messages}
139155
self.manager.invoke(inputs, {"recursion_limit": 1000})

non_src/assets/logo_wide_3.jpg

145 KB
Loading
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
This file contains a manual test scenario for the Manager agent.
3+
It uses a dummy finish_project_planning tool to simulate task completion without running the actual coding pipeline.
4+
"""
5+
6+
from src.tools.tools_project_manager import add_task, modify_task, reorder_tasks
7+
from src.tools.tools_coder_pipeline import prepare_list_dir_tool, prepare_see_file_tool, ask_human_tool
8+
from manager import Manager
9+
from langchain.tools import tool
10+
from typing_extensions import Annotated
11+
from src.utilities.print_formatters import print_formatted
12+
13+
@tool
14+
def dummy_finish_project_planning(dummy: Annotated[str, "Type 'ok' to proceed."]):
15+
"""Dummy version of finish_project_planning that simulates task completion without running the actual pipeline."""
16+
print_formatted("🔄 Simulating task completion...", color="yellow")
17+
return "Task completed successfully"
18+
19+
class TestManager(Manager):
20+
def prepare_tools(self):
21+
"""Override prepare_tools to use dummy finish_project_planning"""
22+
list_dir = prepare_list_dir_tool(self.work_dir)
23+
see_file = prepare_see_file_tool(self.work_dir)
24+
tools = [
25+
add_task,
26+
modify_task,
27+
reorder_tasks,
28+
ask_human_tool,
29+
dummy_finish_project_planning,
30+
list_dir,
31+
see_file,
32+
]
33+
return tools
34+
35+
if __name__ == "__main__":
36+
print_formatted("🧪 Starting Manager Test Scenario 1", color="green")
37+
test_manager = TestManager()
38+
test_manager.run()
39+
print_formatted("✅ Test scenario completed", color="green")

single_task_coder.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,18 @@
1919
from src.utilities.start_project_functions import set_up_dot_clean_coder_dir
2020
from src.utilities.util_functions import create_frontend_feedback_story
2121
from src.tools.rag.rag_utils import update_descriptions
22-
from src.tools.rag.index_file_descriptions import prompt_index_project_files, upsert_file_list, write_file_descriptions, write_file_chunks_descriptions
23-
from src.tools.rag.retrieval import vdb_available
22+
from src.tools.rag.index_file_descriptions import prompt_index_project_files
2423
from src.linters.static_analisys import python_static_analysis
2524

2625

2726
use_frontend_feedback = bool(os.getenv("FRONTEND_URL"))
2827

2928

30-
31-
def run_clean_coder_pipeline(task: str, work_dir: str, doc_harvest: bool = False):
32-
researcher = Researcher(work_dir)
29+
def run_clean_coder_pipeline(task: str, work_dir: str, task_id: str=None):
30+
researcher = Researcher(task_id=task_id)
3331
files, image_paths = researcher.research_task(task)
34-
documentation = None
35-
if doc_harvest:
36-
harvester = Doc_harvester()
37-
documentation = harvester.find_documentation(task, work_dir)
3832

39-
plan = planning(task, files, image_paths, work_dir, documentation=documentation)
33+
plan = planning(task, files, image_paths, work_dir)
4034

4135
executor = Executor(files, work_dir)
4236

@@ -72,7 +66,7 @@ def run_clean_coder_pipeline(task: str, work_dir: str, doc_harvest: bool = False
7266
if __name__ == "__main__":
7367
work_dir = os.getenv("WORK_DIR")
7468
if not work_dir:
75-
raise Exception("WORK_DIR variable not provided. Please add WORK_DIR to .env file")
69+
raise Exception("WORK_DIR variable is not provided. Please add WORK_DIR to .env file")
7670
set_up_dot_clean_coder_dir(work_dir)
7771
prompt_index_project_files()
7872
task = user_input("Provide task to be executed. ")

src/agents/researcher_agent.py

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from src.utilities.objects import CodeFile
33
from typing_extensions import Annotated
44
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
5-
from langgraph.graph import StateGraph
5+
from langgraph.graph import StateGraph, END
66
from dotenv import load_dotenv, find_dotenv
77
from langchain_core.tools import tool
88
from src.tools.tools_coder_pipeline import (
@@ -11,7 +11,14 @@
1111
retrieve_files_by_semantic_query,
1212
)
1313
from src.tools.rag.retrieval import vdb_available
14-
from src.utilities.util_functions import list_directory_tree, read_coderrules, load_prompt
14+
from src.utilities.util_functions import (
15+
list_directory_tree,
16+
read_coderrules,
17+
load_prompt,
18+
save_state_history_to_disk,
19+
load_state_history_from_disk,
20+
join_paths,
21+
)
1522
from src.utilities.langgraph_common_functions import (
1623
call_model,
1724
call_tool,
@@ -21,6 +28,7 @@
2128
)
2229
from src.utilities.print_formatters import print_formatted
2330
from src.utilities.llms import init_llms_medium_intelligence
31+
2432
import os
2533

2634

@@ -52,27 +60,22 @@ class AgentState(TypedDict):
5260
messages: Sequence[BaseMessage]
5361

5462

55-
# Logic for conditional edges
56-
def after_agent_condition(state):
57-
messages = [msg for msg in state["messages"] if msg.type in ["ai", "human"]]
58-
last_message = messages[-1]
59-
60-
if last_message.content == no_tools_msg:
61-
return "agent"
62-
elif last_message.tool_calls[0]["name"] == "final_response_researcher":
63-
return "human"
64-
else:
65-
return "agent"
66-
67-
6863
class Researcher:
69-
def __init__(self, work_dir):
64+
def __init__(self, silent=False, task_id=None):
65+
self.task_id = task_id
66+
self.silent = silent
7067
see_file = prepare_see_file_tool(work_dir)
7168
list_dir = prepare_list_dir_tool(work_dir)
7269
self.tools = [see_file, list_dir, final_response_researcher]
7370
if vdb_available():
7471
self.tools.append(retrieve_files_by_semantic_query)
7572
self.llms = init_llms_medium_intelligence(self.tools, "Researcher")
73+
# Try to load previous research session for this task (if any)
74+
self.prev_messages: List[BaseMessage] = []
75+
if task_id:
76+
history_file = join_paths(work_dir, ".clean_coder", f"research_history_task_{task_id}.json")
77+
if os.path.exists(history_file):
78+
self.prev_messages = load_state_history_from_disk(history_file)
7679

7780
# workflow definition
7881
researcher_workflow = StateGraph(AgentState)
@@ -82,14 +85,14 @@ def __init__(self, work_dir):
8285

8386
researcher_workflow.set_entry_point("agent")
8487

85-
researcher_workflow.add_conditional_edges("agent", after_agent_condition)
88+
researcher_workflow.add_conditional_edges("agent", self.after_agent_condition)
8689
researcher_workflow.add_conditional_edges("human", after_ask_human_condition)
8790

8891
self.researcher = researcher_workflow.compile()
8992

9093
# node functions
9194
def call_model_researcher(self, state):
92-
state = call_model(state, self.llms)
95+
state = call_model(state, self.llms, printing=not self.silent)
9396
last_message = state["messages"][-1]
9497
if len(last_message.tool_calls) == 0:
9598
state["messages"].append(HumanMessage(content=no_tools_msg))
@@ -102,16 +105,41 @@ def call_model_researcher(self, state):
102105
state = call_tool(state, self.tools)
103106
return state
104107

108+
# condition functions
109+
def after_agent_condition(self, state):
110+
messages = [msg for msg in state["messages"] if msg.type in ["ai", "human"]]
111+
last_message = messages[-1]
112+
113+
if last_message.content == no_tools_msg:
114+
return "agent"
115+
elif last_message.tool_calls[0]["name"] == "final_response_researcher":
116+
if self.silent:
117+
state["messages"].append(HumanMessage(content="Approved automatically")) # Dummy message to fullfil state, to align with "Approved by human" message in loun mode
118+
# Save research history to file
119+
history_file = os.path.join(work_dir, ".clean_coder", f"research_history_task_{self.task_id}.json")
120+
save_state_history_to_disk(state, history_file)
121+
return END # Skip human approval in silent mode
122+
return "human"
123+
else:
124+
return "agent"
125+
105126
# just functions
106127
def research_task(self, task):
107-
print_formatted("Researcher starting its work", color="green")
108-
print_formatted("👋 Hey! I'm looking for files on which we will work on together!", color="light_blue")
128+
if not self.silent:
129+
print_formatted("Researcher starting its work", color="green")
130+
print_formatted("👋 Hey! I'm looking for files on which we will work on together!", color="light_blue")
109131

110132
system_prompt_template = load_prompt("researcher_system")
111-
system_message = system_prompt_template.format(task=task, project_rules=read_coderrules())
112-
inputs = {
113-
"messages": [SystemMessage(content=system_message), HumanMessage(content=list_directory_tree(work_dir))]
114-
}
133+
system_message = SystemMessage(
134+
content=system_prompt_template.format(task=task, project_rules=read_coderrules())
135+
)
136+
137+
# Continue previous dialogue if available; otherwise start fresh
138+
if self.prev_messages:
139+
messages = [system_message] + self.prev_messages
140+
else:
141+
messages = [system_message, HumanMessage(content=list_directory_tree(work_dir))]
142+
inputs = {"messages": messages}
115143
researcher_response = self.researcher.invoke(inputs, {"recursion_limit": 100})["messages"][-3]
116144
response_args = researcher_response.tool_calls[0]["args"]
117145
text_files = set(CodeFile(f) for f in response_args["files_to_work_on"] + response_args["reference_files"])
@@ -122,5 +150,5 @@ def research_task(self, task):
122150

123151
if __name__ == "__main__":
124152
task = """Check all system"""
125-
researcher = Researcher(work_dir)
153+
researcher = Researcher()
126154
researcher.research_task(task)

0 commit comments

Comments
 (0)