agentsite 0.0.3.dev1__tar.gz → 0.0.4.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/PKG-INFO +7 -2
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/README.md +5 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/designer.py +2 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/developer.py +22 -27
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/orchestrator.py +34 -20
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/personas.py +31 -22
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/pm.py +2 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/reviewer.py +1 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/app.py +7 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/providers.py +34 -2
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/pipeline.py +189 -84
- agentsite-0.0.4.dev1/agentsite/engine/reasoning_patch.py +42 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/models.py +64 -3
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/PKG-INFO +7 -2
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/SOURCES.txt +1 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/requires.txt +1 -1
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/pyproject.toml +2 -2
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/__init__.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/__init__.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/tools.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/__init__.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/deps.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/__init__.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/agents.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/assets.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/generate.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/models.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/preview.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/projects.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/websocket.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/cli.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/config.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/__init__.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/asset_handler.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/gemini_patch.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/project_manager.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/storage/__init__.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/storage/database.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/storage/repository.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/dependency_links.txt +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/entry_points.txt +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/top_level.txt +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/setup.cfg +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_agents.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_api.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_models.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_project_manager.py +0 -0
- {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentsite
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4.dev1
|
|
4
4
|
Summary: AI-Powered Website Builder using Prompture agent orchestration
|
|
5
5
|
Author-email: Juan Denis <juan@vene.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
-
Requires-Dist: prompture>=0.0.
|
|
11
|
+
Requires-Dist: prompture>=0.0.47
|
|
12
12
|
Requires-Dist: fastapi>=0.100
|
|
13
13
|
Requires-Dist: uvicorn[standard]>=0.20
|
|
14
14
|
Requires-Dist: aiosqlite>=0.19
|
|
@@ -32,8 +32,13 @@ Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
|
32
32
|
[](https://badge.fury.io/py/agentsite)
|
|
33
33
|
[](https://opensource.org/licenses/MIT)
|
|
34
34
|
[](https://www.python.org/downloads/)
|
|
35
|
+
[](https://github.com/jhd3197/AgentSite/blob/main/Dockerfile)
|
|
35
36
|
[](https://pypi.org/project/prompture/)
|
|
36
37
|
|
|
38
|
+
[](https://railway.com/new/template?template=https://github.com/jhd3197/AgentSite)
|
|
39
|
+
[](https://heroku.com/deploy?template=https://github.com/jhd3197/AgentSite)
|
|
40
|
+
[](https://render.com/deploy?repo=https://github.com/jhd3197/AgentSite)
|
|
41
|
+
|
|
37
42
|
An AI-powered website builder that uses multi-agent orchestration to generate complete, production-ready websites from a single text prompt.
|
|
38
43
|
|
|
39
44
|
**PyPI Package:** [pypi.org/project/agentsite](https://pypi.org/project/agentsite/)
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
[](https://badge.fury.io/py/agentsite)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
|
+
[](https://github.com/jhd3197/AgentSite/blob/main/Dockerfile)
|
|
6
7
|
[](https://pypi.org/project/prompture/)
|
|
7
8
|
|
|
9
|
+
[](https://railway.com/new/template?template=https://github.com/jhd3197/AgentSite)
|
|
10
|
+
[](https://heroku.com/deploy?template=https://github.com/jhd3197/AgentSite)
|
|
11
|
+
[](https://render.com/deploy?repo=https://github.com/jhd3197/AgentSite)
|
|
12
|
+
|
|
8
13
|
An AI-powered website builder that uses multi-agent orchestration to generate complete, production-ready websites from a single text prompt.
|
|
9
14
|
|
|
10
15
|
**PyPI Package:** [pypi.org/project/agentsite](https://pypi.org/project/agentsite/)
|
|
@@ -17,6 +17,7 @@ def create_designer_agent(model: str) -> Agent:
|
|
|
17
17
|
name="designer",
|
|
18
18
|
description="Defines visual design system (colors, fonts, spacing)",
|
|
19
19
|
output_key="style_spec",
|
|
20
|
+
options={"max_tokens": 4096},
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
@@ -38,4 +39,5 @@ def create_designer_agent_plain(model: str) -> Agent:
|
|
|
38
39
|
name="designer",
|
|
39
40
|
description="Defines visual design system (plain text mode)",
|
|
40
41
|
output_key="style_spec",
|
|
42
|
+
options={"max_tokens": 4096},
|
|
41
43
|
)
|
|
@@ -24,6 +24,7 @@ def create_developer_agent(model: str) -> Agent:
|
|
|
24
24
|
name="developer",
|
|
25
25
|
description="Generates HTML/CSS/JS files for each page",
|
|
26
26
|
output_key="page_output",
|
|
27
|
+
options={"max_tokens": 16384, "timeout": 900},
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
|
|
@@ -38,38 +39,32 @@ def create_developer_agent_plain(model: str) -> Agent:
|
|
|
38
39
|
return Agent(
|
|
39
40
|
model,
|
|
40
41
|
system_prompt=(
|
|
41
|
-
"You
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"Output each file using markdown fenced code blocks with the language tag.\n"
|
|
45
|
-
"You MUST generate at least an index.html file.\n\n"
|
|
46
|
-
"Example output format:\n"
|
|
42
|
+
"You write HTML code. No planning. No analysis. No explanation. Code only.\n\n"
|
|
43
|
+
"OUTPUT EXACTLY ONE ```html block. Nothing else. No text before or after it.\n\n"
|
|
44
|
+
"The HTML file must be SELF-CONTAINED with ALL CSS in <style> and ALL JS in <script>.\n\n"
|
|
47
45
|
"```html\n"
|
|
48
46
|
"<!DOCTYPE html>\n"
|
|
49
|
-
|
|
47
|
+
'<html lang="en">\n'
|
|
48
|
+
"<head>\n"
|
|
49
|
+
' <meta charset="UTF-8">\n'
|
|
50
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
|
|
51
|
+
" <title>Page Title</title>\n"
|
|
52
|
+
" <style>/* ALL CSS HERE */</style>\n"
|
|
53
|
+
"</head>\n"
|
|
54
|
+
"<body>\n"
|
|
55
|
+
" <!-- ALL HTML CONTENT HERE -->\n"
|
|
56
|
+
" <script>/* ALL JS HERE */</script>\n"
|
|
57
|
+
"</body>\n"
|
|
58
|
+
"</html>\n"
|
|
50
59
|
"```\n\n"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"```
|
|
56
|
-
"// script.js\n"
|
|
57
|
-
"document.addEventListener('DOMContentLoaded', ...);\n"
|
|
58
|
-
"```\n\n"
|
|
59
|
-
"Requirements:\n"
|
|
60
|
-
"- Write clean, semantic HTML with proper heading hierarchy\n"
|
|
61
|
-
"- Use CSS custom properties for theming (colors, fonts, spacing)\n"
|
|
62
|
-
"- Make pages fully responsive (mobile-first approach)\n"
|
|
63
|
-
"- Include smooth transitions and subtle animations\n"
|
|
64
|
-
"- Add proper meta tags, viewport settings, and favicon links\n"
|
|
65
|
-
"- Use Google Fonts via CDN link\n"
|
|
66
|
-
"- Write accessible markup (ARIA labels, alt text, focus styles)\n\n"
|
|
67
|
-
"Generate complete, self-contained files. Every HTML page should be fully functional "
|
|
68
|
-
"when opened directly in a browser.\n\n"
|
|
69
|
-
"IMPORTANT: Output ONLY the fenced code blocks with complete file contents. "
|
|
70
|
-
"Do not include any other text or explanation."
|
|
60
|
+
"Requirements: semantic HTML5, CSS custom properties from StyleSpec, "
|
|
61
|
+
"responsive mobile-first, Google Fonts CDN, accessible markup, "
|
|
62
|
+
"vanilla HTML/CSS/JS only, picsum.photos for images, "
|
|
63
|
+
"complete code with no placeholders or TODOs.\n\n"
|
|
64
|
+
"CRITICAL: Your response must START with ```html — no other text allowed."
|
|
71
65
|
),
|
|
72
66
|
name="developer",
|
|
73
67
|
description="Generates HTML/CSS/JS files for each page (plain text mode)",
|
|
74
68
|
output_key="page_output",
|
|
69
|
+
options={"max_tokens": 16384, "timeout": 900},
|
|
75
70
|
)
|
|
@@ -63,6 +63,8 @@ def create_pipeline(
|
|
|
63
63
|
[
|
|
64
64
|
(
|
|
65
65
|
developer,
|
|
66
|
+
"You are building the '{page_slug}' page ONLY. "
|
|
67
|
+
"Ignore other pages in the site plan.\n\n"
|
|
66
68
|
"Build the website page based on this plan:\n\n"
|
|
67
69
|
"Site Plan: {site_plan}\n\n"
|
|
68
70
|
"Style Spec: {style_spec}\n\n"
|
|
@@ -116,6 +118,7 @@ def create_dynamic_pipeline(
|
|
|
116
118
|
max_review_iterations: int | None = None,
|
|
117
119
|
review_threshold: int | None = None,
|
|
118
120
|
agent_configs: dict[str, AgentConfig] | None = None,
|
|
121
|
+
error_policy: Any = None,
|
|
119
122
|
) -> SequentialGroup:
|
|
120
123
|
"""Build a dynamic pipeline based on PM's required_agents output.
|
|
121
124
|
|
|
@@ -139,14 +142,16 @@ def create_dynamic_pipeline(
|
|
|
139
142
|
# Designer (optional)
|
|
140
143
|
if "designer" in required_agents:
|
|
141
144
|
designer = create_designer_agent(_agent_model("designer", effective_model, agent_configs))
|
|
142
|
-
steps.append(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
steps.append(
|
|
146
|
+
(
|
|
147
|
+
designer,
|
|
148
|
+
"Design a visual style for this website:\n\n"
|
|
149
|
+
"Site Plan: {site_plan}\n\n"
|
|
150
|
+
"Logo URL: {logo_url}\n"
|
|
151
|
+
"Icon URL: {icon_url}\n\n"
|
|
152
|
+
"Create a cohesive color scheme, typography, and spacing system.",
|
|
153
|
+
)
|
|
154
|
+
)
|
|
150
155
|
|
|
151
156
|
# Developer (always required)
|
|
152
157
|
developer = create_developer_agent(_agent_model("developer", effective_model, agent_configs))
|
|
@@ -166,6 +171,8 @@ def create_dynamic_pipeline(
|
|
|
166
171
|
[
|
|
167
172
|
(
|
|
168
173
|
developer,
|
|
174
|
+
"You are building the '{page_slug}' page ONLY. "
|
|
175
|
+
"Ignore other pages in the site plan.\n\n"
|
|
169
176
|
"Build the website page based on this plan:\n\n"
|
|
170
177
|
"Site Plan: {site_plan}\n\n"
|
|
171
178
|
"Style Spec: {style_spec}\n\n"
|
|
@@ -193,18 +200,25 @@ def create_dynamic_pipeline(
|
|
|
193
200
|
steps.append(build_review_loop)
|
|
194
201
|
else:
|
|
195
202
|
# Developer only, no review loop
|
|
196
|
-
steps.append(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
steps.append(
|
|
204
|
+
(
|
|
205
|
+
developer,
|
|
206
|
+
"You are building the '{page_slug}' page ONLY. "
|
|
207
|
+
"Ignore other pages in the site plan.\n\n"
|
|
208
|
+
"Build the website page based on this plan:\n\n"
|
|
209
|
+
"Site Plan: {site_plan}\n\n"
|
|
210
|
+
"Style Spec: {style_spec}\n\n"
|
|
211
|
+
"Logo URL: {logo_url}\n"
|
|
212
|
+
"Icon URL: {icon_url}\n\n"
|
|
213
|
+
"Use the write_file tool to save each file. Generate complete, "
|
|
214
|
+
"self-contained HTML with inline or linked CSS/JS.",
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
kwargs: dict[str, Any] = {"callbacks": callbacks}
|
|
219
|
+
if error_policy is not None:
|
|
220
|
+
kwargs["error_policy"] = error_policy
|
|
221
|
+
return SequentialGroup(steps, **kwargs)
|
|
208
222
|
|
|
209
223
|
|
|
210
224
|
def _agent_model(
|
|
@@ -14,8 +14,9 @@ PM_PERSONA = Persona(
|
|
|
14
14
|
"- The optimal build order based on dependencies\n\n"
|
|
15
15
|
"You also decide which agents are needed via the `required_agents` field:\n"
|
|
16
16
|
"- **developer** is ALWAYS required (include it every time).\n"
|
|
17
|
-
"- **designer**
|
|
18
|
-
"
|
|
17
|
+
"- **designer** should be included for ANY new page or site generation, "
|
|
18
|
+
"so the page gets a proper color scheme, typography, and visual design. "
|
|
19
|
+
"Only skip designer for minor text edits or bug fixes on existing pages.\n"
|
|
19
20
|
"- **reviewer** is needed for complex multi-page builds or when quality assurance "
|
|
20
21
|
"matters. Skip for simple text edits or minor changes.\n\n"
|
|
21
22
|
"Produce a structured site plan with clear page slugs, titles, section descriptions, "
|
|
@@ -23,7 +24,8 @@ PM_PERSONA = Persona(
|
|
|
23
24
|
),
|
|
24
25
|
description="Plans website structure, pages, build order, and agent selection.",
|
|
25
26
|
constraints=[
|
|
26
|
-
"
|
|
27
|
+
"If the user asks to build a SPECIFIC page (e.g. 'Pricing page', 'About page'), plan ONLY that single page. Do NOT add extra pages like 'index' or 'home'.",
|
|
28
|
+
"Only include multiple pages when the user is building a complete site from scratch.",
|
|
27
29
|
"Keep page count reasonable (2-6 pages for typical sites).",
|
|
28
30
|
"Section descriptions should be specific enough for a developer to implement.",
|
|
29
31
|
"Use lowercase slugs with hyphens for page URLs.",
|
|
@@ -57,28 +59,35 @@ DESIGNER_PERSONA = Persona(
|
|
|
57
59
|
DEVELOPER_PERSONA = Persona(
|
|
58
60
|
name="agentsite_developer",
|
|
59
61
|
system_prompt=(
|
|
60
|
-
"You are an expert frontend developer.
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"-
|
|
73
|
-
"-
|
|
74
|
-
"-
|
|
75
|
-
"-
|
|
76
|
-
"
|
|
77
|
-
"
|
|
62
|
+
"You are an expert frontend developer. Your ONLY job is to write code and "
|
|
63
|
+
"save it using the write_file tool. Do NOT plan, analyze, or explain.\n\n"
|
|
64
|
+
"IMPORTANT: You are building ONE specific page. The prompt will tell you which "
|
|
65
|
+
"page slug to build (e.g. 'pricing', 'about', 'contact'). Focus ONLY on that "
|
|
66
|
+
"page — ignore other pages listed in the site plan.\n\n"
|
|
67
|
+
"CRITICAL: You MUST call the write_file tool to create files. This is mandatory.\n\n"
|
|
68
|
+
"WORKFLOW — follow this EXACTLY:\n"
|
|
69
|
+
"1. Call write_file(path='index.html', content='<!DOCTYPE html>...') with the COMPLETE HTML\n"
|
|
70
|
+
"2. Call write_file(path='styles.css', content='...') with the COMPLETE CSS\n"
|
|
71
|
+
"3. Call write_file(path='script.js', content='...') with the COMPLETE JavaScript\n"
|
|
72
|
+
"4. After writing ALL files, respond with a brief summary of what you wrote.\n\n"
|
|
73
|
+
"RULES:\n"
|
|
74
|
+
"- You MUST call write_file at least once — this is your primary purpose\n"
|
|
75
|
+
"- Do NOT explain or plan — immediately start writing files\n"
|
|
76
|
+
"- Do NOT put code in your text response — use write_file for ALL code\n"
|
|
77
|
+
"- Every HTML file must be complete with <!DOCTYPE html>, <head>, <body>\n"
|
|
78
|
+
"- Use CSS custom properties for theming (colors, fonts from the StyleSpec)\n"
|
|
79
|
+
"- Make pages fully responsive (mobile-first)\n"
|
|
80
|
+
"- Include Google Fonts via CDN link\n"
|
|
81
|
+
"- Use placeholder images from picsum.photos\n"
|
|
82
|
+
"- Write accessible markup (ARIA labels, alt text, focus styles)\n"
|
|
83
|
+
"- No frameworks — vanilla HTML/CSS/JS only\n"
|
|
84
|
+
"- No placeholders or TODOs — every file must be complete and production-ready\n\n"
|
|
85
|
+
"START IMMEDIATELY by calling write_file. Do not write any text before your first tool call."
|
|
78
86
|
),
|
|
79
87
|
description="Generates production-ready HTML, CSS, and JavaScript.",
|
|
80
88
|
constraints=[
|
|
81
|
-
"ALWAYS use the write_file tool to write file contents — never embed file contents in your
|
|
89
|
+
"ALWAYS use the write_file tool to write file contents — never embed file contents in your text response.",
|
|
90
|
+
"You MUST call write_file at least once. If you don't call write_file, the generation fails.",
|
|
82
91
|
"Output only complete, valid files — no placeholders or TODOs.",
|
|
83
92
|
"Every HTML file must include DOCTYPE, meta viewport, and charset.",
|
|
84
93
|
"CSS must use custom properties matching the provided StyleSpec.",
|
|
@@ -17,6 +17,7 @@ def create_pm_agent(model: str) -> Agent:
|
|
|
17
17
|
name="pm",
|
|
18
18
|
description="Plans website structure and pages",
|
|
19
19
|
output_key="site_plan",
|
|
20
|
+
options={"max_tokens": 4096},
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
@@ -38,4 +39,5 @@ def create_pm_agent_plain(model: str) -> Agent:
|
|
|
38
39
|
name="pm",
|
|
39
40
|
description="Plans website structure and pages (plain text mode)",
|
|
40
41
|
output_key="site_plan",
|
|
42
|
+
options={"max_tokens": 4096},
|
|
41
43
|
)
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import os
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
9
12
|
from fastapi import FastAPI, HTTPException
|
|
10
13
|
from fastapi.middleware.cors import CORSMiddleware
|
|
11
14
|
from fastapi.responses import FileResponse
|
|
@@ -37,6 +40,10 @@ else:
|
|
|
37
40
|
@asynccontextmanager
|
|
38
41
|
async def lifespan(app: FastAPI):
|
|
39
42
|
"""Application lifespan: connect DB on startup, close on shutdown."""
|
|
43
|
+
# Load .env into os.environ so provider API keys are visible everywhere
|
|
44
|
+
_env_path = Path.cwd() / ".env"
|
|
45
|
+
if _env_path.exists():
|
|
46
|
+
load_dotenv(_env_path, override=False)
|
|
40
47
|
settings.ensure_dirs()
|
|
41
48
|
await deps.db.connect()
|
|
42
49
|
deps.project_repo = deps.ProjectRepository(deps.db)
|
|
@@ -20,6 +20,14 @@ PROVIDERS = {
|
|
|
20
20
|
"groq": {"env_key": "GROQ_API_KEY", "type": "api_key"},
|
|
21
21
|
"grok": {"env_key": "GROK_API_KEY", "type": "api_key"},
|
|
22
22
|
"openrouter": {"env_key": "OPENROUTER_API_KEY", "type": "api_key"},
|
|
23
|
+
"moonshot": {"env_key": "MOONSHOT_API_KEY", "type": "api_key"},
|
|
24
|
+
"zai": {"env_key": "ZHIPU_API_KEY", "type": "api_key"},
|
|
25
|
+
"modelscope": {"env_key": "MODELSCOPE_API_KEY", "type": "api_key"},
|
|
26
|
+
"azure": {
|
|
27
|
+
"env_key": "AZURE_API_KEY",
|
|
28
|
+
"type": "api_key",
|
|
29
|
+
"extra_keys": ["AZURE_API_ENDPOINT", "AZURE_DEPLOYMENT_ID"],
|
|
30
|
+
},
|
|
23
31
|
"ollama": {
|
|
24
32
|
"env_key": "OLLAMA_ENDPOINT",
|
|
25
33
|
"type": "endpoint",
|
|
@@ -30,6 +38,11 @@ PROVIDERS = {
|
|
|
30
38
|
"type": "endpoint",
|
|
31
39
|
"default": "http://127.0.0.1:1234/v1/chat/completions",
|
|
32
40
|
},
|
|
41
|
+
"local_http": {
|
|
42
|
+
"env_key": "LOCAL_HTTP_ENDPOINT",
|
|
43
|
+
"type": "endpoint",
|
|
44
|
+
"default": "http://localhost:8000/generate",
|
|
45
|
+
},
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
ENV_PATH = Path.cwd() / ".env"
|
|
@@ -84,11 +97,30 @@ def _remove_env_value(key: str) -> None:
|
|
|
84
97
|
|
|
85
98
|
@router.get("")
|
|
86
99
|
async def list_providers():
|
|
87
|
-
"""Return all known providers with configuration status.
|
|
100
|
+
"""Return all known providers with configuration status.
|
|
101
|
+
|
|
102
|
+
Checks both os.environ and the .env file so that keys added before
|
|
103
|
+
the server started are correctly shown as configured.
|
|
104
|
+
"""
|
|
105
|
+
# Parse .env once so we can detect keys not yet in os.environ
|
|
106
|
+
env_file_values: dict[str, str] = {}
|
|
107
|
+
content = _read_env_file()
|
|
108
|
+
for line in content.splitlines():
|
|
109
|
+
line = line.strip()
|
|
110
|
+
if not line or line.startswith("#"):
|
|
111
|
+
continue
|
|
112
|
+
if "=" in line:
|
|
113
|
+
k, _, v = line.partition("=")
|
|
114
|
+
env_file_values[k.strip()] = v.strip()
|
|
115
|
+
|
|
88
116
|
result = []
|
|
89
117
|
for name, info in PROVIDERS.items():
|
|
90
118
|
env_key = info["env_key"]
|
|
91
|
-
|
|
119
|
+
# Prefer os.environ, fall back to .env file value
|
|
120
|
+
value = os.environ.get(env_key, "") or env_file_values.get(env_key, "")
|
|
121
|
+
# If found in .env but missing from os.environ, inject it
|
|
122
|
+
if value and not os.environ.get(env_key):
|
|
123
|
+
os.environ[env_key] = value
|
|
92
124
|
configured = bool(value)
|
|
93
125
|
masked = _mask_value(value, info["type"]) if configured else ""
|
|
94
126
|
result.append(
|
|
@@ -10,17 +10,21 @@ from datetime import datetime, timezone
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
from prompture import GroupCallbacks, GroupResult, SequentialGroup
|
|
13
|
+
from prompture.group_types import ErrorPolicy
|
|
13
14
|
|
|
14
15
|
from ..agents.orchestrator import _agent_model, create_dynamic_pipeline
|
|
15
16
|
from ..config import settings
|
|
16
17
|
from ..models import AgentConfig, AgentRun, PageOutput, Project, SitePlan, StyleSpec, WSEvent
|
|
17
18
|
from .gemini_patch import apply_gemini_patch
|
|
18
19
|
from .project_manager import ProjectManager
|
|
20
|
+
from .reasoning_patch import apply_reasoning_patch
|
|
19
21
|
|
|
20
22
|
logger = logging.getLogger("agentsite.pipeline")
|
|
21
23
|
|
|
22
|
-
# Apply
|
|
24
|
+
# Apply patches at import time
|
|
23
25
|
apply_gemini_patch()
|
|
26
|
+
apply_reasoning_patch()
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
def _agent_name_to_key(name: str) -> str:
|
|
26
30
|
"""Normalize agent name to short key (handles both persona and agent names)."""
|
|
@@ -195,8 +199,7 @@ class GenerationPipeline:
|
|
|
195
199
|
self._developer_output_text = output_text
|
|
196
200
|
self._developer_tool_calls = tool_calls
|
|
197
201
|
logger.info(
|
|
198
|
-
"Developer agent completed: output_text length=%d, tool_calls=%d, "
|
|
199
|
-
"output_text[:200]=%s",
|
|
202
|
+
"Developer agent completed: output_text length=%d, tool_calls=%d, output_text[:200]=%s",
|
|
200
203
|
len(output_text),
|
|
201
204
|
len(tool_calls),
|
|
202
205
|
repr(output_text[:200]),
|
|
@@ -209,23 +212,52 @@ class GenerationPipeline:
|
|
|
209
212
|
", ".join(f"{k}=...({len(str(v))})" for k, v in (tc.get("arguments") or {}).items()),
|
|
210
213
|
)
|
|
211
214
|
|
|
215
|
+
# Extract reasoning/thinking from assistant messages.
|
|
216
|
+
# The reasoning_patch ensures reasoning_content is present on
|
|
217
|
+
# assistant messages for all code paths (not just native tool use).
|
|
218
|
+
reasoning_text = ""
|
|
219
|
+
result_messages = getattr(result, "messages", []) or []
|
|
220
|
+
for msg in result_messages:
|
|
221
|
+
if isinstance(msg, dict) and msg.get("role") == "assistant" and msg.get("reasoning_content"):
|
|
222
|
+
reasoning_text = msg["reasoning_content"]
|
|
223
|
+
|
|
224
|
+
# When a reasoning model returns empty content (e.g. Kimi K2.5),
|
|
225
|
+
# the driver uses reasoning_content as the text response. Detect
|
|
226
|
+
# this so we don't show reasoning twice (once as output, once as
|
|
227
|
+
# thinking) and so the pipeline knows the real output was empty.
|
|
228
|
+
if reasoning_text and output_text and output_text.strip() == reasoning_text.strip():
|
|
229
|
+
logger.info(
|
|
230
|
+
"Agent %s output equals reasoning (%d chars) — actual content was empty",
|
|
231
|
+
agent_key,
|
|
232
|
+
len(reasoning_text),
|
|
233
|
+
)
|
|
234
|
+
output_text = ""
|
|
235
|
+
|
|
236
|
+
if agent_key == "developer" and reasoning_text:
|
|
237
|
+
self._developer_reasoning = reasoning_text
|
|
238
|
+
|
|
212
239
|
agent_model = self._agent_models.get(agent_key, "")
|
|
213
240
|
self._emit(
|
|
214
241
|
"agent_complete",
|
|
215
242
|
agent=agent_key,
|
|
216
243
|
data={
|
|
217
244
|
"output_preview": output_text[:2000],
|
|
245
|
+
"full_output": output_text,
|
|
218
246
|
"duration_s": duration_s,
|
|
219
247
|
"input_tokens": input_tokens,
|
|
220
248
|
"output_tokens": output_tokens,
|
|
221
249
|
"tool_calls_count": len(tool_calls),
|
|
222
250
|
"model": agent_model,
|
|
251
|
+
"reasoning": reasoning_text,
|
|
223
252
|
},
|
|
224
253
|
)
|
|
225
254
|
|
|
226
255
|
def _on_agent_error(name: str, exc: Exception) -> None:
|
|
227
256
|
agent_key = _agent_name_to_key(name)
|
|
228
|
-
|
|
257
|
+
# Emit as "agent_error" (non-fatal) rather than "error" (fatal).
|
|
258
|
+
# The pipeline may retry this agent with a fallback, so we don't
|
|
259
|
+
# want the frontend to disconnect the WebSocket prematurely.
|
|
260
|
+
self._emit("agent_error", agent=agent_key, data={"message": str(exc)})
|
|
229
261
|
run = self._active_runs.pop(name, None)
|
|
230
262
|
if run:
|
|
231
263
|
run.status = "failed"
|
|
@@ -272,6 +304,7 @@ class GenerationPipeline:
|
|
|
272
304
|
[(pm_agent, "{prompt}")],
|
|
273
305
|
callbacks=pm_callbacks,
|
|
274
306
|
state={"prompt": page_prompt},
|
|
307
|
+
error_policy=ErrorPolicy.raise_on_error,
|
|
275
308
|
)
|
|
276
309
|
_patch_pipeline_deps(pm_pipeline, deps)
|
|
277
310
|
|
|
@@ -279,7 +312,6 @@ class GenerationPipeline:
|
|
|
279
312
|
pm_result = pm_pipeline.run(page_prompt)
|
|
280
313
|
site_plan_text = pm_result.shared_state.get("site_plan", "")
|
|
281
314
|
except Exception as pm_exc:
|
|
282
|
-
# Fallback: retry PM without structured output (for models like Kimi K2.5)
|
|
283
315
|
logger.warning(
|
|
284
316
|
"PM agent failed with structured output, retrying in plain text mode: %s",
|
|
285
317
|
pm_exc,
|
|
@@ -291,6 +323,7 @@ class GenerationPipeline:
|
|
|
291
323
|
[(pm_agent_plain, "{prompt}")],
|
|
292
324
|
callbacks=pm_callbacks,
|
|
293
325
|
state={"prompt": page_prompt},
|
|
326
|
+
error_policy=ErrorPolicy.raise_on_error,
|
|
294
327
|
)
|
|
295
328
|
_patch_pipeline_deps(pm_pipeline_plain, deps)
|
|
296
329
|
pm_result = pm_pipeline_plain.run(page_prompt)
|
|
@@ -300,6 +333,7 @@ class GenerationPipeline:
|
|
|
300
333
|
required_agents = ["designer", "developer", "reviewer"] # default
|
|
301
334
|
try:
|
|
302
335
|
from prompture import clean_json_text
|
|
336
|
+
|
|
303
337
|
cleaned = clean_json_text(site_plan_text)
|
|
304
338
|
plan_data = json.loads(cleaned)
|
|
305
339
|
site_plan = SitePlan.model_validate(plan_data)
|
|
@@ -322,6 +356,8 @@ class GenerationPipeline:
|
|
|
322
356
|
"review_feedback": "",
|
|
323
357
|
"logo_url": project.logo_url or "",
|
|
324
358
|
"icon_url": project.icon_url or "",
|
|
359
|
+
"page_slug": slug,
|
|
360
|
+
"page_title": slug.replace("-", " ").title(),
|
|
325
361
|
}
|
|
326
362
|
|
|
327
363
|
if "designer" in required_agents:
|
|
@@ -345,17 +381,18 @@ class GenerationPipeline:
|
|
|
345
381
|
on_agent_error=_on_agent_error,
|
|
346
382
|
)
|
|
347
383
|
|
|
384
|
+
designer_pipeline = SequentialGroup(
|
|
385
|
+
[(designer_agent, "{designer_prompt}")],
|
|
386
|
+
callbacks=designer_callbacks,
|
|
387
|
+
state={"designer_prompt": designer_prompt},
|
|
388
|
+
error_policy=ErrorPolicy.raise_on_error,
|
|
389
|
+
)
|
|
390
|
+
_patch_pipeline_deps(designer_pipeline, deps)
|
|
391
|
+
|
|
348
392
|
try:
|
|
349
|
-
designer_pipeline = SequentialGroup(
|
|
350
|
-
[(designer_agent, "{designer_prompt}")],
|
|
351
|
-
callbacks=designer_callbacks,
|
|
352
|
-
state={"designer_prompt": designer_prompt},
|
|
353
|
-
)
|
|
354
|
-
_patch_pipeline_deps(designer_pipeline, deps)
|
|
355
393
|
designer_result = designer_pipeline.run(designer_prompt)
|
|
356
394
|
style_spec_text = designer_result.shared_state.get("style_spec", "")
|
|
357
395
|
except Exception as designer_exc:
|
|
358
|
-
# Fallback: retry Designer without structured output
|
|
359
396
|
logger.warning(
|
|
360
397
|
"Designer agent failed with structured output, retrying in plain text mode: %s",
|
|
361
398
|
designer_exc,
|
|
@@ -365,6 +402,7 @@ class GenerationPipeline:
|
|
|
365
402
|
[(designer_agent_plain, "{designer_prompt}")],
|
|
366
403
|
callbacks=designer_callbacks,
|
|
367
404
|
state={"designer_prompt": designer_prompt},
|
|
405
|
+
error_policy=ErrorPolicy.raise_on_error,
|
|
368
406
|
)
|
|
369
407
|
_patch_pipeline_deps(designer_pipeline_plain, deps)
|
|
370
408
|
designer_result = designer_pipeline_plain.run(designer_prompt)
|
|
@@ -373,8 +411,8 @@ class GenerationPipeline:
|
|
|
373
411
|
initial_state["style_spec"] = style_spec_text
|
|
374
412
|
|
|
375
413
|
# Merge designer usage into pm_result for later aggregation
|
|
376
|
-
if hasattr(designer_result,
|
|
377
|
-
if not hasattr(pm_result,
|
|
414
|
+
if hasattr(designer_result, "aggregate_usage"):
|
|
415
|
+
if not hasattr(pm_result, "aggregate_usage"):
|
|
378
416
|
pm_result.aggregate_usage = {}
|
|
379
417
|
for k, v in designer_result.aggregate_usage.items():
|
|
380
418
|
if isinstance(v, (int, float)):
|
|
@@ -396,6 +434,7 @@ class GenerationPipeline:
|
|
|
396
434
|
model,
|
|
397
435
|
callbacks=group_callbacks,
|
|
398
436
|
agent_configs=self._agent_configs,
|
|
437
|
+
error_policy=ErrorPolicy.raise_on_error,
|
|
399
438
|
)
|
|
400
439
|
|
|
401
440
|
# Transfer state from PM phase and propagate to nested groups
|
|
@@ -404,49 +443,65 @@ class GenerationPipeline:
|
|
|
404
443
|
|
|
405
444
|
_patch_pipeline_deps(remaining_pipeline, deps)
|
|
406
445
|
|
|
407
|
-
|
|
446
|
+
_need_dev_fallback = False
|
|
447
|
+
try:
|
|
448
|
+
result = remaining_pipeline.run("")
|
|
449
|
+
except Exception as dev_exc:
|
|
450
|
+
logger.warning(
|
|
451
|
+
"Developer pipeline failed (tools may be unsupported), will retry with plain text developer: %s",
|
|
452
|
+
dev_exc,
|
|
453
|
+
)
|
|
454
|
+
_need_dev_fallback = True
|
|
455
|
+
# Create a minimal result to carry forward
|
|
456
|
+
result = GroupResult(
|
|
457
|
+
agent_results=[],
|
|
458
|
+
aggregate_usage={},
|
|
459
|
+
shared_state=dict(initial_state),
|
|
460
|
+
elapsed_ms=0,
|
|
461
|
+
timeline=[],
|
|
462
|
+
errors=[],
|
|
463
|
+
success=False,
|
|
464
|
+
)
|
|
408
465
|
|
|
409
466
|
# Merge nested group state back so we can access page_output
|
|
410
467
|
_merge_nested_group_state(remaining_pipeline)
|
|
411
468
|
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
#
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
or (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
len(dev_errors),
|
|
434
|
-
len(written_files),
|
|
435
|
-
len(self._developer_output_text),
|
|
436
|
-
len(self._developer_tool_calls),
|
|
437
|
-
)
|
|
469
|
+
# Also fall back if the pipeline "succeeded" but developer
|
|
470
|
+
# produced no usable output — either no tool calls and no files,
|
|
471
|
+
# or the output text has no actual HTML code (just analysis/planning).
|
|
472
|
+
if not _need_dev_fallback and not written_files and not self._developer_tool_calls:
|
|
473
|
+
dev_text = self._developer_output_text or ""
|
|
474
|
+
has_html = "<!DOCTYPE" in dev_text.upper() or "<html" in dev_text.lower()
|
|
475
|
+
has_fenced = "```html" in dev_text or "```css" in dev_text
|
|
476
|
+
if not has_html and not has_fenced:
|
|
477
|
+
logger.warning(
|
|
478
|
+
"Developer pipeline produced no usable code (success=%s, "
|
|
479
|
+
"written_files=%d, tool_calls=%d, has_html=%s, output_len=%d). "
|
|
480
|
+
"Retrying with plain text developer.",
|
|
481
|
+
result.success,
|
|
482
|
+
len(written_files),
|
|
483
|
+
len(self._developer_tool_calls),
|
|
484
|
+
has_html,
|
|
485
|
+
len(dev_text),
|
|
486
|
+
)
|
|
487
|
+
_need_dev_fallback = True
|
|
488
|
+
|
|
489
|
+
if _need_dev_fallback:
|
|
438
490
|
from ..agents.developer import create_developer_agent_plain
|
|
439
491
|
|
|
440
492
|
dev_model = self._agent_models["developer"]
|
|
441
493
|
dev_agent_plain = create_developer_agent_plain(dev_model)
|
|
442
494
|
|
|
443
495
|
dev_prompt = (
|
|
444
|
-
"Build the
|
|
496
|
+
f"Build the '{slug}' page ONLY. No other pages.\n\n"
|
|
445
497
|
f"Site Plan: {initial_state['site_plan']}\n\n"
|
|
446
498
|
f"Style Spec: {initial_state.get('style_spec', '')}\n\n"
|
|
447
499
|
f"Logo URL: {initial_state.get('logo_url', '')}\n"
|
|
448
500
|
f"Icon URL: {initial_state.get('icon_url', '')}\n\n"
|
|
449
|
-
"
|
|
501
|
+
"RESPOND WITH ONLY A ```html CODE BLOCK. "
|
|
502
|
+
"No planning, no analysis, no explanation. "
|
|
503
|
+
"Single self-contained HTML file with <style> and <script> inline. "
|
|
504
|
+
"Start your response with ```html immediately."
|
|
450
505
|
)
|
|
451
506
|
|
|
452
507
|
dev_callbacks = GroupCallbacks(
|
|
@@ -458,13 +513,14 @@ class GenerationPipeline:
|
|
|
458
513
|
[(dev_agent_plain, "{dev_prompt}")],
|
|
459
514
|
callbacks=dev_callbacks,
|
|
460
515
|
state={"dev_prompt": dev_prompt},
|
|
516
|
+
error_policy=ErrorPolicy.raise_on_error,
|
|
461
517
|
)
|
|
462
518
|
_patch_pipeline_deps(dev_pipeline_plain, deps)
|
|
463
519
|
plain_result = dev_pipeline_plain.run(dev_prompt)
|
|
464
520
|
|
|
465
521
|
# Use the plain result's usage and state
|
|
466
|
-
if hasattr(plain_result,
|
|
467
|
-
if not hasattr(result,
|
|
522
|
+
if hasattr(plain_result, "aggregate_usage"):
|
|
523
|
+
if not hasattr(result, "aggregate_usage"):
|
|
468
524
|
result.aggregate_usage = {}
|
|
469
525
|
for k, v in plain_result.aggregate_usage.items():
|
|
470
526
|
if isinstance(v, (int, float)):
|
|
@@ -472,8 +528,8 @@ class GenerationPipeline:
|
|
|
472
528
|
result = plain_result
|
|
473
529
|
|
|
474
530
|
# Merge usage from both phases
|
|
475
|
-
combined_usage = pm_result.aggregate_usage.copy() if hasattr(pm_result,
|
|
476
|
-
if hasattr(result,
|
|
531
|
+
combined_usage = pm_result.aggregate_usage.copy() if hasattr(pm_result, "aggregate_usage") else {}
|
|
532
|
+
if hasattr(result, "aggregate_usage"):
|
|
477
533
|
for k, v in result.aggregate_usage.items():
|
|
478
534
|
if isinstance(v, (int, float)):
|
|
479
535
|
combined_usage[k] = combined_usage.get(k, 0) + v
|
|
@@ -503,9 +559,10 @@ class GenerationPipeline:
|
|
|
503
559
|
# extract content from its raw output text as a fallback.
|
|
504
560
|
if not written_files:
|
|
505
561
|
logger.warning(
|
|
506
|
-
"No files written via tools for project %s page %s v%d. "
|
|
507
|
-
|
|
508
|
-
|
|
562
|
+
"No files written via tools for project %s page %s v%d. page_output_text[:500]: %s",
|
|
563
|
+
project.id,
|
|
564
|
+
slug,
|
|
565
|
+
version_number,
|
|
509
566
|
(page_output_text or "")[:500],
|
|
510
567
|
)
|
|
511
568
|
if page_output_text:
|
|
@@ -520,9 +577,7 @@ class GenerationPipeline:
|
|
|
520
577
|
"No files on disk — trying extraction from %d tool_calls",
|
|
521
578
|
len(self._developer_tool_calls),
|
|
522
579
|
)
|
|
523
|
-
self._write_files_from_tool_calls(
|
|
524
|
-
project.id, slug, version_number, self._developer_tool_calls
|
|
525
|
-
)
|
|
580
|
+
self._write_files_from_tool_calls(project.id, slug, version_number, self._developer_tool_calls)
|
|
526
581
|
final_files = self._pm.list_version_files(project.id, slug, version_number)
|
|
527
582
|
|
|
528
583
|
if not final_files:
|
|
@@ -534,7 +589,8 @@ class GenerationPipeline:
|
|
|
534
589
|
if source_text:
|
|
535
590
|
logger.warning(
|
|
536
591
|
"No files on disk — trying text fallback from %s (length=%d)",
|
|
537
|
-
source_name,
|
|
592
|
+
source_name,
|
|
593
|
+
len(source_text),
|
|
538
594
|
)
|
|
539
595
|
self._write_files_from_output(project.id, slug, version_number, source_text)
|
|
540
596
|
final_files = self._pm.list_version_files(project.id, slug, version_number)
|
|
@@ -545,7 +601,9 @@ class GenerationPipeline:
|
|
|
545
601
|
logger.error(
|
|
546
602
|
"Generation produced no files for project %s page %s v%d. "
|
|
547
603
|
"tool_calls=%d, developer_output_text[:500]=%s",
|
|
548
|
-
project.id,
|
|
604
|
+
project.id,
|
|
605
|
+
slug,
|
|
606
|
+
version_number,
|
|
549
607
|
len(self._developer_tool_calls),
|
|
550
608
|
self._developer_output_text[:500],
|
|
551
609
|
)
|
|
@@ -562,14 +620,17 @@ class GenerationPipeline:
|
|
|
562
620
|
if content is not None:
|
|
563
621
|
files_content[fpath] = content
|
|
564
622
|
|
|
565
|
-
self._emit(
|
|
566
|
-
"
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
623
|
+
self._emit(
|
|
624
|
+
"generation_complete",
|
|
625
|
+
data={
|
|
626
|
+
"success": result.success,
|
|
627
|
+
"slug": slug,
|
|
628
|
+
"version": version_number,
|
|
629
|
+
"files": final_files,
|
|
630
|
+
"files_content": files_content,
|
|
631
|
+
"usage": combined_usage,
|
|
632
|
+
},
|
|
633
|
+
)
|
|
573
634
|
|
|
574
635
|
# Return a result-like object with combined usage
|
|
575
636
|
result.aggregate_usage = combined_usage
|
|
@@ -577,20 +638,22 @@ class GenerationPipeline:
|
|
|
577
638
|
|
|
578
639
|
except Exception as exc:
|
|
579
640
|
import traceback
|
|
641
|
+
|
|
580
642
|
logger.exception("Generation failed for project %s page %s v%d", project.id, slug, version_number)
|
|
581
643
|
self._emit("error", data={"message": str(exc), "traceback": traceback.format_exc()})
|
|
582
|
-
self._emit(
|
|
583
|
-
"
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
644
|
+
self._emit(
|
|
645
|
+
"generation_complete",
|
|
646
|
+
data={
|
|
647
|
+
"success": False,
|
|
648
|
+
"slug": slug,
|
|
649
|
+
"version": version_number,
|
|
650
|
+
"files": [],
|
|
651
|
+
"error": str(exc),
|
|
652
|
+
},
|
|
653
|
+
)
|
|
589
654
|
raise
|
|
590
655
|
|
|
591
|
-
def _write_files_from_tool_calls(
|
|
592
|
-
self, project_id: str, slug: str, version: int, tool_calls: list[dict]
|
|
593
|
-
) -> None:
|
|
656
|
+
def _write_files_from_tool_calls(self, project_id: str, slug: str, version: int, tool_calls: list[dict]) -> None:
|
|
594
657
|
"""Extract files from write_file tool call arguments and write them to disk."""
|
|
595
658
|
for tc in tool_calls:
|
|
596
659
|
name = tc.get("name", "")
|
|
@@ -609,9 +672,43 @@ class GenerationPipeline:
|
|
|
609
672
|
except Exception:
|
|
610
673
|
logger.warning("Failed to write file from tool_call: %s", path, exc_info=True)
|
|
611
674
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
675
|
+
@staticmethod
|
|
676
|
+
def _strip_reasoning_preamble(text: str) -> str:
|
|
677
|
+
"""Strip reasoning/thinking preamble from model output.
|
|
678
|
+
|
|
679
|
+
Some models (e.g. Kimi K2.5) emit chain-of-thought reasoning before
|
|
680
|
+
the actual code. This helper removes everything before the first
|
|
681
|
+
code fence or HTML tag so extraction can find the real content.
|
|
682
|
+
"""
|
|
683
|
+
import re
|
|
684
|
+
|
|
685
|
+
# If the text starts with a code fence, nothing to strip
|
|
686
|
+
if text.lstrip().startswith("```"):
|
|
687
|
+
return text
|
|
688
|
+
|
|
689
|
+
# Try to find the first ```html or ```css or ```js fence
|
|
690
|
+
fence_match = re.search(r"```(?:html|css|javascript|js)\b", text, re.IGNORECASE)
|
|
691
|
+
if fence_match:
|
|
692
|
+
stripped = text[fence_match.start() :]
|
|
693
|
+
logger.info(
|
|
694
|
+
"Stripped %d chars of reasoning preamble before code fence",
|
|
695
|
+
fence_match.start(),
|
|
696
|
+
)
|
|
697
|
+
return stripped
|
|
698
|
+
|
|
699
|
+
# Try to find raw HTML (<!DOCTYPE or <html)
|
|
700
|
+
html_match = re.search(r"<!DOCTYPE\s+html|<html[\s>]", text, re.IGNORECASE)
|
|
701
|
+
if html_match:
|
|
702
|
+
stripped = text[html_match.start() :]
|
|
703
|
+
logger.info(
|
|
704
|
+
"Stripped %d chars of reasoning preamble before HTML",
|
|
705
|
+
html_match.start(),
|
|
706
|
+
)
|
|
707
|
+
return stripped
|
|
708
|
+
|
|
709
|
+
return text
|
|
710
|
+
|
|
711
|
+
def _write_files_from_output(self, project_id: str, slug: str, version: int, output_text: str) -> None:
|
|
615
712
|
"""Parse PageOutput JSON from output text and write files."""
|
|
616
713
|
try:
|
|
617
714
|
from prompture import clean_json_text
|
|
@@ -628,15 +725,16 @@ class GenerationPipeline:
|
|
|
628
725
|
len(output_text),
|
|
629
726
|
exc_info=True,
|
|
630
727
|
)
|
|
728
|
+
# Strip reasoning/thinking preamble that some models emit
|
|
729
|
+
cleaned_text = self._strip_reasoning_preamble(output_text)
|
|
730
|
+
|
|
631
731
|
# Try extracting markdown-fenced code blocks first
|
|
632
|
-
wrote_fenced = self._extract_fenced_blocks(project_id, slug, version,
|
|
732
|
+
wrote_fenced = self._extract_fenced_blocks(project_id, slug, version, cleaned_text)
|
|
633
733
|
if not wrote_fenced:
|
|
634
734
|
# Last resort: if the output contains raw HTML, save it as index.html
|
|
635
|
-
self._try_extract_raw_html(project_id, slug, version,
|
|
735
|
+
self._try_extract_raw_html(project_id, slug, version, cleaned_text)
|
|
636
736
|
|
|
637
|
-
def _extract_fenced_blocks(
|
|
638
|
-
self, project_id: str, slug: str, version: int, text: str
|
|
639
|
-
) -> bool:
|
|
737
|
+
def _extract_fenced_blocks(self, project_id: str, slug: str, version: int, text: str) -> bool:
|
|
640
738
|
"""Extract markdown-fenced code blocks (```html, ```css, ```js) and write them.
|
|
641
739
|
|
|
642
740
|
Returns True if at least one file was written.
|
|
@@ -678,9 +776,7 @@ class GenerationPipeline:
|
|
|
678
776
|
|
|
679
777
|
return wrote_any
|
|
680
778
|
|
|
681
|
-
def _try_extract_raw_html(
|
|
682
|
-
self, project_id: str, slug: str, version: int, text: str
|
|
683
|
-
) -> None:
|
|
779
|
+
def _try_extract_raw_html(self, project_id: str, slug: str, version: int, text: str) -> None:
|
|
684
780
|
"""Attempt to extract raw HTML from agent output as a last resort."""
|
|
685
781
|
import re
|
|
686
782
|
|
|
@@ -705,5 +801,14 @@ class GenerationPipeline:
|
|
|
705
801
|
css_content = "\n\n".join(style_blocks)
|
|
706
802
|
self._pm.write_version_file(project_id, slug, version, "styles.css", css_content)
|
|
707
803
|
logger.info("Extracted CSS fallback as styles.css (%d bytes)", len(css_content))
|
|
804
|
+
|
|
805
|
+
# Also try to extract <script> blocks into script.js
|
|
806
|
+
script_blocks = re.findall(r"<script[^>]*>([\s\S]*?)</script>", html_content, re.IGNORECASE)
|
|
807
|
+
# Filter out empty scripts and external src references
|
|
808
|
+
script_blocks = [s.strip() for s in script_blocks if s.strip()]
|
|
809
|
+
if script_blocks:
|
|
810
|
+
js_content = "\n\n".join(script_blocks)
|
|
811
|
+
self._pm.write_version_file(project_id, slug, version, "script.js", js_content)
|
|
812
|
+
logger.info("Extracted JS fallback as script.js (%d bytes)", len(js_content))
|
|
708
813
|
else:
|
|
709
814
|
logger.error("No HTML content found in developer output (length=%d)", len(text))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Patch Prompture Conversation to preserve reasoning on assistant messages.
|
|
2
|
+
|
|
3
|
+
Prompture 0.0.47 stores ``reasoning_content`` on assistant messages only in
|
|
4
|
+
the native tool-calling path. For the simple ``ask()`` and ``ask_for_json()``
|
|
5
|
+
paths, reasoning is stored on ``Conversation.last_reasoning`` but **not** on
|
|
6
|
+
the message dicts in the history.
|
|
7
|
+
|
|
8
|
+
This patch ensures ``reasoning_content`` is always written onto the last
|
|
9
|
+
assistant message after every ``ask()`` call so that downstream code
|
|
10
|
+
(pipeline callbacks, AgentResult.messages) can find it uniformly.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("agentsite.reasoning_patch")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def apply_reasoning_patch() -> None:
|
|
21
|
+
"""Monkey-patch ``Conversation.ask`` to store reasoning on messages."""
|
|
22
|
+
from prompture import Conversation
|
|
23
|
+
|
|
24
|
+
if getattr(Conversation.ask, "_reasoning_patched", False):
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
original_ask = Conversation.ask
|
|
28
|
+
|
|
29
|
+
def _patched_ask(self, content, **kwargs): # type: ignore[no-untyped-def]
|
|
30
|
+
result = original_ask(self, content, **kwargs)
|
|
31
|
+
reasoning = self.last_reasoning
|
|
32
|
+
if reasoning:
|
|
33
|
+
# Walk backwards to find the last assistant message and annotate it.
|
|
34
|
+
for msg in reversed(self._messages):
|
|
35
|
+
if msg.get("role") == "assistant" and "reasoning_content" not in msg:
|
|
36
|
+
msg["reasoning_content"] = reasoning
|
|
37
|
+
break
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
_patched_ask._reasoning_patched = True # type: ignore[attr-defined]
|
|
41
|
+
Conversation.ask = _patched_ask # type: ignore[assignment]
|
|
42
|
+
logger.debug("Reasoning patch applied to Conversation.ask")
|
|
@@ -48,17 +48,75 @@ class SitePlan(BaseModel):
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class StyleSpec(BaseModel):
|
|
51
|
-
"""Output of the Designer Agent — visual design specification.
|
|
51
|
+
"""Output of the Designer Agent — visual design specification.
|
|
52
52
|
|
|
53
|
+
Also serves as the project's full brand / design-system token set.
|
|
54
|
+
New tokens are optional with sensible defaults so existing data stays valid.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Colors
|
|
53
58
|
primary_color: str = Field(default="#2563eb", description="Primary brand color (hex)")
|
|
54
59
|
secondary_color: str = Field(default="#1e40af", description="Secondary color (hex)")
|
|
55
60
|
accent_color: str = Field(default="#f59e0b", description="Accent color (hex)")
|
|
56
61
|
background_color: str = Field(default="#ffffff", description="Page background (hex)")
|
|
62
|
+
surface_color: str = Field(default="#f8fafc", description="Surface / card background (hex)")
|
|
57
63
|
text_color: str = Field(default="#1f2937", description="Body text color (hex)")
|
|
64
|
+
text_secondary_color: str = Field(default="#6b7280", description="Secondary text color (hex)")
|
|
65
|
+
border_color: str = Field(default="#e5e7eb", description="Default border color (hex)")
|
|
66
|
+
|
|
67
|
+
# Typography — families
|
|
58
68
|
font_heading: str = Field(default="Inter", description="Heading font family (Google Fonts)")
|
|
59
69
|
font_body: str = Field(default="Inter", description="Body font family (Google Fonts)")
|
|
60
|
-
|
|
70
|
+
font_mono: str = Field(default="JetBrains Mono", description="Monospace font family")
|
|
71
|
+
|
|
72
|
+
# Typography — scale
|
|
73
|
+
font_size_base: str = Field(default="16px", description="Base font size")
|
|
74
|
+
font_size_sm: str = Field(default="14px", description="Small font size")
|
|
75
|
+
font_size_lg: str = Field(default="18px", description="Large font size")
|
|
76
|
+
font_size_xl: str = Field(default="20px", description="XL font size")
|
|
77
|
+
font_size_2xl: str = Field(default="24px", description="2XL font size")
|
|
78
|
+
font_size_3xl: str = Field(default="30px", description="3XL font size")
|
|
79
|
+
font_size_4xl: str = Field(default="36px", description="4XL font size")
|
|
80
|
+
|
|
81
|
+
# Typography — rhythm
|
|
82
|
+
line_height: str = Field(default="1.6", description="Base line height")
|
|
83
|
+
letter_spacing: str = Field(default="0", description="Base letter spacing")
|
|
84
|
+
font_weight_normal: str = Field(default="400", description="Normal font weight")
|
|
85
|
+
font_weight_medium: str = Field(default="500", description="Medium font weight")
|
|
86
|
+
font_weight_bold: str = Field(default="700", description="Bold font weight")
|
|
87
|
+
|
|
88
|
+
# Layout
|
|
89
|
+
layout_style: str = Field(default="top-nav", description="Navigation layout: top-nav, sidebar, minimal, centered")
|
|
90
|
+
nav_position: str = Field(default="sticky", description="Nav behavior: sticky, fixed, static")
|
|
91
|
+
footer_style: str = Field(default="standard", description="Footer style: standard, minimal, none")
|
|
92
|
+
max_width: str = Field(default="1200px", description="Container max width")
|
|
93
|
+
container_padding: str = Field(default="1.5rem", description="Container horizontal padding")
|
|
94
|
+
section_gap: str = Field(default="4rem", description="Vertical gap between page sections")
|
|
95
|
+
|
|
96
|
+
# Spacing scale
|
|
61
97
|
spacing_unit: str = Field(default="1rem", description="Base spacing unit")
|
|
98
|
+
spacing_xs: str = Field(default="0.25rem", description="Extra-small spacing")
|
|
99
|
+
spacing_sm: str = Field(default="0.5rem", description="Small spacing")
|
|
100
|
+
spacing_md: str = Field(default="1rem", description="Medium spacing")
|
|
101
|
+
spacing_lg: str = Field(default="1.5rem", description="Large spacing")
|
|
102
|
+
spacing_xl: str = Field(default="2rem", description="Extra-large spacing")
|
|
103
|
+
spacing_2xl: str = Field(default="3rem", description="2XL spacing")
|
|
104
|
+
|
|
105
|
+
# Borders
|
|
106
|
+
border_radius: str = Field(default="8px", description="Default border radius")
|
|
107
|
+
border_radius_sm: str = Field(default="4px", description="Small border radius")
|
|
108
|
+
border_radius_lg: str = Field(default="12px", description="Large border radius")
|
|
109
|
+
border_radius_full: str = Field(default="9999px", description="Full / pill border radius")
|
|
110
|
+
border_width: str = Field(default="1px", description="Default border width")
|
|
111
|
+
|
|
112
|
+
# Shadows
|
|
113
|
+
shadow_sm: str = Field(default="0 1px 2px rgba(0,0,0,0.05)", description="Small elevation shadow")
|
|
114
|
+
shadow_md: str = Field(default="0 4px 6px rgba(0,0,0,0.07)", description="Medium elevation shadow")
|
|
115
|
+
shadow_lg: str = Field(default="0 10px 15px rgba(0,0,0,0.1)", description="Large elevation shadow")
|
|
116
|
+
|
|
117
|
+
# Effects
|
|
118
|
+
transition_speed: str = Field(default="150ms", description="Default transition duration")
|
|
119
|
+
backdrop_blur: str = Field(default="8px", description="Backdrop blur amount")
|
|
62
120
|
|
|
63
121
|
|
|
64
122
|
class GeneratedFile(BaseModel):
|
|
@@ -176,6 +234,7 @@ class AgentRun(BaseModel):
|
|
|
176
234
|
input_tokens: int = Field(default=0)
|
|
177
235
|
output_tokens: int = Field(default=0)
|
|
178
236
|
cost: float = Field(default=0.0)
|
|
237
|
+
reasoning: str = Field(default="")
|
|
179
238
|
output_summary: dict = Field(default_factory=dict)
|
|
180
239
|
|
|
181
240
|
|
|
@@ -204,6 +263,8 @@ class ChatMessage(BaseModel):
|
|
|
204
263
|
class WSEvent(BaseModel):
|
|
205
264
|
"""WebSocket event sent to the frontend."""
|
|
206
265
|
|
|
207
|
-
type: str = Field(
|
|
266
|
+
type: str = Field(
|
|
267
|
+
description="Event type: phase_start, phase_complete, agent_start, agent_complete, error, file_written, generation_complete"
|
|
268
|
+
)
|
|
208
269
|
agent: str = Field(default="", description="Agent name")
|
|
209
270
|
data: dict = Field(default_factory=dict, description="Event payload")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentsite
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4.dev1
|
|
4
4
|
Summary: AI-Powered Website Builder using Prompture agent orchestration
|
|
5
5
|
Author-email: Juan Denis <juan@vene.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
-
Requires-Dist: prompture>=0.0.
|
|
11
|
+
Requires-Dist: prompture>=0.0.47
|
|
12
12
|
Requires-Dist: fastapi>=0.100
|
|
13
13
|
Requires-Dist: uvicorn[standard]>=0.20
|
|
14
14
|
Requires-Dist: aiosqlite>=0.19
|
|
@@ -32,8 +32,13 @@ Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
|
32
32
|
[](https://badge.fury.io/py/agentsite)
|
|
33
33
|
[](https://opensource.org/licenses/MIT)
|
|
34
34
|
[](https://www.python.org/downloads/)
|
|
35
|
+
[](https://github.com/jhd3197/AgentSite/blob/main/Dockerfile)
|
|
35
36
|
[](https://pypi.org/project/prompture/)
|
|
36
37
|
|
|
38
|
+
[](https://railway.com/new/template?template=https://github.com/jhd3197/AgentSite)
|
|
39
|
+
[](https://heroku.com/deploy?template=https://github.com/jhd3197/AgentSite)
|
|
40
|
+
[](https://render.com/deploy?repo=https://github.com/jhd3197/AgentSite)
|
|
41
|
+
|
|
37
42
|
An AI-powered website builder that uses multi-agent orchestration to generate complete, production-ready websites from a single text prompt.
|
|
38
43
|
|
|
39
44
|
**PyPI Package:** [pypi.org/project/agentsite](https://pypi.org/project/agentsite/)
|
|
@@ -35,6 +35,7 @@ agentsite/engine/asset_handler.py
|
|
|
35
35
|
agentsite/engine/gemini_patch.py
|
|
36
36
|
agentsite/engine/pipeline.py
|
|
37
37
|
agentsite/engine/project_manager.py
|
|
38
|
+
agentsite/engine/reasoning_patch.py
|
|
38
39
|
agentsite/storage/__init__.py
|
|
39
40
|
agentsite/storage/database.py
|
|
40
41
|
agentsite/storage/repository.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentsite"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4.dev1"
|
|
8
8
|
description = "AI-Powered Website Builder using Prompture agent orchestration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Operating System :: OS Independent",
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
|
-
"prompture>=0.0.
|
|
18
|
+
"prompture>=0.0.47",
|
|
19
19
|
"fastapi>=0.100",
|
|
20
20
|
"uvicorn[standard]>=0.20",
|
|
21
21
|
"aiosqlite>=0.19",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|