Add wizard agent and project type str to Project

This commit is contained in:
mijauexe
2025-02-16 12:01:53 +01:00
parent b33e6d4774
commit d4b4a99ba1
14 changed files with 221 additions and 123 deletions

136
config-extension.json Normal file
View File

@@ -0,0 +1,136 @@
{
"llm": {
"openai": {
"base_url": null,
"api_key": null,
"connect_timeout": 60.0,
"read_timeout": 60.0,
"extra": null
},
"anthropic": {
"base_url": null,
"api_key": null,
"connect_timeout": 60.0,
"read_timeout": 60.0,
"extra": null
}
},
"agent": {
"default": {
"provider": "openai",
"model": "gpt-4o-2024-05-13",
"temperature": 0.5
},
"BugHunter.check_logs": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.5
},
"CodeMonkey": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.0
},
"CodeMonkey.code_review": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.0
},
"CodeMonkey.describe_files": {
"provider": "openai",
"model": "gpt-4o-mini-2024-07-18",
"temperature": 0.0
},
"Frontend": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.0
},
"get_relevant_files": {
"provider": "openai",
"model": "gpt-4o-2024-05-13",
"temperature": 0.5
},
"Developer.parse_task": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.0
},
"SpecWriter": {
"provider": "openai",
"model": "gpt-4-0125-preview",
"temperature": 0.0
},
"Developer.breakdown_current_task": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.5
},
"TechLead.plan_epic": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.5
},
"TechLead.epic_breakdown": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.5
},
"Troubleshooter.generate_bug_report": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.5
},
"Troubleshooter.get_run_command": {
"provider": "openai",
"model": "claude-3-7-sonnet-20250219",
"temperature": 0.0
}
},
"prompt": {
"paths": [
"/Users/sven/projects/pythagora/pythagora-core/core/prompts"
]
},
"log": {
"level": "DEBUG",
"format": "%(asctime)s %(levelname)s [%(name)s] %(message)s",
"output": "pythagora.log"
},
"db": {
"url": "sqlite+aiosqlite:///data/database/pythagora.db",
"debug_sql": false
},
"ui": {
"type": "plain"
},
"fs": {
"type": "local",
"workspace_root": "/Users/sven/projects/pythagora/pythagora-core/workspace",
"ignore_paths": [
".git",
".gpt-pilot",
".idea",
".vscode",
".next",
".DS_Store",
"__pycache__",
"site-packages",
"node_modules",
"package-lock.json",
"venv",
".venv",
"dist",
"build",
"target",
"*.min.js",
"*.min.css",
"*.svg",
"*.csv",
"*.log",
"go.sum",
"migration_lock.toml"
],
"ignore_size_threshold": 50000
}
}

View File

@@ -1,7 +1,3 @@
import asyncio
import secrets
from uuid import uuid4
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
from core.agents.git import GitMixin
@@ -11,7 +7,6 @@ from core.config import FRONTEND_AGENT_NAME
from core.llm.parser import DescriptiveCodeBlockParser
from core.log import get_logger
from core.telemetry import telemetry
from core.templates.registry import PROJECT_TEMPLATES
from core.ui.base import ProjectStage
log = get_logger(__name__)
@@ -28,9 +23,7 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
display_name = "Frontend"
async def run(self) -> AgentResponse:
if not self.current_state.epics:
finished = await self.init_frontend()
elif not self.current_state.epics[0]["messages"]:
if not self.current_state.epics[0]["messages"]:
finished = await self.start_frontend()
elif not self.next_state.epics[-1].get("fe_iteration_done"):
finished = await self.continue_frontend()
@@ -40,63 +33,6 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
return await self.end_frontend_iteration(finished)
async def init_frontend(self) -> bool:
"""
Builds frontend of the app.
:return: AgentResponse.done(self)
"""
self.next_state.action = FE_INIT
await self.ui.send_project_stage({"stage": ProjectStage.PROJECT_DESCRIPTION})
auth_needed = await self.ask_question(
"Do you need authentication in your app (login, register, etc.)?",
buttons={
"yes": "Yes",
"no": "No",
},
buttons_only=True,
default="no",
)
self.state_manager.template = {}
options = {
"auth": auth_needed.button == "yes",
"jwt_secret": secrets.token_hex(32),
"refresh_token_secret": secrets.token_hex(32),
}
self.state_manager.template["options"] = options
if not self.state_manager.async_tasks:
self.state_manager.async_tasks = []
self.state_manager.async_tasks.append(asyncio.create_task(self.apply_template(options)))
self.next_state.knowledge_base["user_options"] = options
self.state_manager.user_options = options
description = await self.ask_question(
"Please describe the app you want to build.",
allow_empty=False,
full_screen=True,
)
description = description.text.strip()
self.state_manager.template["description"] = description
self.next_state.specification.description = description
self.next_state.epics = [
{
"id": uuid4().hex,
"name": "Build frontend",
"source": "frontend",
"description": description,
"messages": [],
"summary": None,
"completed": False,
}
]
return False
async def start_frontend(self):
"""
Starts the frontend of the app.
@@ -298,29 +234,6 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
return AgentResponse.done(self)
async def apply_template(self, options: dict = {}):
"""
Applies a template to the frontend.
"""
template_name = "vite_react"
template_class = PROJECT_TEMPLATES.get(template_name)
if not template_class:
log.error(f"Project template not found: {template_name}")
return
template = template_class(
options,
self.state_manager,
self.process_manager,
)
self.state_manager.template["template"] = template
log.info(f"Applying project template: {template.name}")
summary = await template.apply()
self.next_state.relevant_files = template.relevant_files
self.next_state.modified_files = {}
self.next_state.specification.template_summary = summary
async def set_app_details(self):
"""
Sets the app details.

View File

@@ -24,6 +24,7 @@ from core.agents.task_completer import TaskCompleter
from core.agents.tech_lead import TechLead
from core.agents.tech_writer import TechnicalWriter
from core.agents.troubleshooter import Troubleshooter
from core.agents.wizard import Wizard
from core.db.models.project_state import IterationStatus, TaskStatus
from core.log import get_logger
from core.telemetry import telemetry
@@ -392,7 +393,9 @@ class Orchestrator(BaseAgent, GitMixin):
if prev_response.type == ResponseType.UPDATE_SPECIFICATION:
return SpecWriter(self.state_manager, self.ui, prev_response=prev_response)
if not state.epics or (state.current_epic and state.current_epic.get("source") == "frontend"):
if not state.epics:
return Wizard(self.state_manager, self.ui, process_manager=self.process_manager)
elif state.current_epic and state.current_epic.get("source") == "frontend":
# Build frontend
return Frontend(self.state_manager, self.ui, process_manager=self.process_manager)
elif not state.specification.description:

View File

@@ -202,6 +202,7 @@ async def list_projects_json(db: SessionManager):
last_updated = None
p = {
"name": project.name,
"project_type": project.project_type,
"id": project.id.hex,
"branches": [],
}

View File

@@ -129,7 +129,11 @@ async def start_new_project(sm: StateManager, ui: UIBase) -> bool:
stack = await ui.ask_question(
"What do you want to use to build your app?",
allow_empty=False,
buttons={"node": "Node.js", "other": "Other (coming soon)"},
buttons={
"node": "Node.js",
"swagger": "Frontend only with OpenAPI (Swagger) backend",
"other": "Other (coming soon)",
},
buttons_only=True,
source=pythagora_source,
full_screen=True,
@@ -148,16 +152,11 @@ async def start_new_project(sm: StateManager, ui: UIBase) -> bool:
)
await ui.send_message("Thank you for submitting your request to support other languages.")
return False
elif stack.button == "node":
await telemetry.trace_code_event(
"stack-choice",
{"language": "node"},
)
elif stack.button == "python":
await telemetry.trace_code_event(
"stack-choice",
{"language": "python"},
)
await telemetry.trace_code_event(
"stack-choice",
{"language": stack.button},
)
while True:
try:
@@ -182,7 +181,7 @@ async def start_new_project(sm: StateManager, ui: UIBase) -> bool:
else:
break
project_state = await sm.create_project(project_name)
project_state = await sm.create_project(name=project_name, project_type=stack.button)
return project_state is not None

View File

@@ -27,6 +27,7 @@ class Project(Base):
folder_name: Mapped[str] = mapped_column(
default=lambda context: Project.get_folder_from_project_name(context.get_current_parameters()["name"])
)
project_type: Mapped[str] = mapped_column()
# Relationships
branches: Mapped[list["Branch"]] = relationship(back_populates="project", cascade="all", lazy="raise")

View File

@@ -78,7 +78,9 @@ class StateManager:
async with self.session_manager as session:
return await Project.get_all_projects(session)
async def create_project(self, name: str, folder_name: Optional[str] = None) -> Project:
async def create_project(
self, name: str, project_type: Optional[str] = "node", folder_name: Optional[str] = None
) -> Project:
"""
Create a new project and set it as the current one.
@@ -86,7 +88,7 @@ class StateManager:
:return: The Project object.
"""
session = await self.session_manager.start()
project = Project(name=name, folder_name=folder_name)
project = Project(name=name, project_type=project_type, folder_name=folder_name)
branch = Branch(project=project)
state = ProjectState.create_initial_state(branch)
session.add(project)

View File

@@ -1,5 +1,9 @@
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
{% if options.auth_type == "api_key" %}
const API_KEY = import.meta.env.VITE_API_KEY;
{% endif %}
const api = axios.create({
headers: {
'Content-Type': 'application/json',
@@ -10,16 +14,33 @@ const api = axios.create({
});
let accessToken: string | null = null;
{% if options.auth %}
// Axios request interceptor: Attach access token to headers
// Axios request interceptor: Attach access token and API key to headers
api.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
if (!accessToken) {
accessToken = localStorage.getItem('accessToken');
}
if (accessToken && config.headers) {
config.headers.Authorization = `Bearer ${accessToken}`;
// Check if the request is not for login or register
{% if options.auth_type == "api_key" %}
const isAuthEndpoint = config.url?.includes('/login') || config.url?.includes('/register');
if (!isAuthEndpoint) {
// Add API key for non-auth endpoints
if (config.headers && API_KEY) {
config.headers['api_key'] = API_KEY; // or whatever header name your API expects
}
{% endif %}
// Add authorization token if available
if (!accessToken) {
accessToken = localStorage.getItem('accessToken');
}
if (accessToken && config.headers) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
}
return config;
},
(error: AxiosError): Promise<AxiosError> => Promise.reject(error)
@@ -47,6 +68,14 @@ api.interceptors.response.use(
// Retry the original request with the new token
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
// Ensure API key is still present in retry
{% if options.auth_type == "api_key" %}
if (API_KEY) {
originalRequest.headers['api_key'] = API_KEY;
}
{% endif %}
}
return api(originalRequest);
} catch (err) {

View File

@@ -179,9 +179,11 @@ async def test_list_projects_json(mock_StateManager, capsys):
project = MagicMock(
id=MagicMock(hex="abcd"),
# project_type=MagicMock(hex="abcd"),
branches=[branch],
)
project.name = "project1"
project.project_type = "node"
sm.list_projects = AsyncMock(return_value=[project])
await list_projects_json(None)
@@ -194,6 +196,7 @@ async def test_list_projects_json(mock_StateManager, capsys):
{
"name": "project1",
"id": "abcd",
"project_type": "node",
"updated_at": "2021-01-03T00:00:00",
"branches": [
{
@@ -226,6 +229,7 @@ async def test_list_projects(mock_StateManager, capsys):
project = MagicMock(
id="abcd",
project_type="node",
branches=[branch],
)
project.name = "project1"
@@ -304,7 +308,14 @@ async def test_main(mock_Orchestrator, args, run_orchestrator, retval, tmp_path)
with patch("core.cli.helpers.ArgumentParser", new=MockArgumentParser):
ui, db, args = init()
ui.ask_question = AsyncMock(return_value=MagicMock(text="test", cancelled=False))
# Create a mock with a string value for the button attribute
mock_response = MagicMock()
mock_response.text = "test"
mock_response.cancelled = False
# Set a string value for the button attribute
mock_response.button = "node" # or whatever valid project type you want to use
ui.ask_question = AsyncMock(return_value=mock_response)
mock_orca = mock_Orchestrator.return_value
mock_orca.run = AsyncMock(return_value=True)
@@ -330,7 +341,10 @@ async def test_main_handles_crash(mock_Orchestrator, tmp_path):
with patch("core.cli.helpers.ArgumentParser", new=MockArgumentParser):
ui, db, args = init()
ui.ask_question = AsyncMock(return_value=MagicMock(text="test", cancelled=False))
# Create a mock response with a string value for the button attribute
mock_response = MagicMock(text="test", cancelled=False)
mock_response.button = "test_project_type" # Set a string value for project_type
ui.ask_question = AsyncMock(return_value=mock_response)
ui.send_message = AsyncMock()
mock_orca = mock_Orchestrator.return_value

View File

@@ -62,7 +62,7 @@ async def agentcontext(testmanager):
ask_question=AsyncMock(),
)
await sm.create_project("test")
await sm.create_project(name="test", project_type="node")
mock_llm = None

View File

@@ -13,6 +13,6 @@ def create_project_state(project_name="Test Project", branch_name=Branch.DEFAULT
:return: The ProjectState object.
"""
project = Project(name=project_name)
project = Project(name=project_name, project_type="node")
branch = Branch(name=branch_name, project=project)
return ProjectState.create_initial_state(branch)

View File

@@ -22,7 +22,7 @@ async def test_get_by_id_no_match(testdb):
@pytest.mark.asyncio
async def test_get_by_id(testdb):
project = Project(name="test")
project = Project(name="test", project_type="node")
branch = Branch(project=project)
testdb.add(project)
await testdb.commit()
@@ -34,7 +34,7 @@ async def test_get_by_id(testdb):
@pytest.mark.asyncio
async def test_get_last_state_no_steps(testdb):
project = Project(name="test")
project = Project(name="test", project_type="node")
branch = Branch(project=project)
testdb.add(project)
await testdb.commit()
@@ -61,7 +61,7 @@ async def test_get_last_state(testdb):
@pytest.mark.asyncio
async def test_get_last_state_no_session():
project = Project(name="test")
project = Project(name="test", project_type="node")
branch = Branch(project=project)
with pytest.raises(ValueError):

View File

@@ -22,7 +22,7 @@ async def test_get_by_id_no_match(testdb):
@pytest.mark.asyncio
async def test_get_by_id(testdb):
project = Project(name="test")
project = Project(name="test", project_type="node")
testdb.add(project)
await testdb.commit()
@@ -32,7 +32,7 @@ async def test_get_by_id(testdb):
@pytest.mark.asyncio
async def test_delete_by_id(testdb):
project = Project(name="test")
project = Project(name="test", project_type="node")
testdb.add(project)
await testdb.commit()
@@ -43,7 +43,7 @@ async def test_delete_by_id(testdb):
@pytest.mark.asyncio
async def test_get_branch_no_match(testdb):
project = Project(name="test")
project = Project(name="test", project_type="node")
testdb.add(project)
await testdb.commit()
@@ -53,7 +53,7 @@ async def test_get_branch_no_match(testdb):
@pytest.mark.asyncio
async def test_get_branch(testdb):
project = Project(name="test")
project = Project(name="test", project_type="node")
branch = Branch(project=project)
testdb.add(project)
testdb.add(branch)
@@ -65,7 +65,7 @@ async def test_get_branch(testdb):
@pytest.mark.asyncio
async def test_get_branch_no_session():
project = Project(name="test")
project = Project(name="test", project_type="node")
with pytest.raises(ValueError):
await project.get_branch()
@@ -86,7 +86,7 @@ async def test_get_all_projects(testdb):
@pytest.mark.asyncio
async def test_default_folder_name(testdb):
project = Project(name="test project")
project = Project(name="test project", project_type="node")
testdb.add(project)
await testdb.commit()

View File

@@ -20,7 +20,7 @@ async def test_get_by_id(testdb):
@pytest.mark.asyncio
async def test_get_last_state_no_session():
project = Project(name="test")
project = Project(name="test", project_type="node")
branch = Branch(project=project)
with pytest.raises(ValueError):