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.
Files changed (48) hide show
  1. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/PKG-INFO +7 -2
  2. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/README.md +5 -0
  3. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/designer.py +2 -0
  4. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/developer.py +22 -27
  5. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/orchestrator.py +34 -20
  6. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/personas.py +31 -22
  7. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/pm.py +2 -0
  8. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/reviewer.py +1 -0
  9. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/app.py +7 -0
  10. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/providers.py +34 -2
  11. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/pipeline.py +189 -84
  12. agentsite-0.0.4.dev1/agentsite/engine/reasoning_patch.py +42 -0
  13. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/models.py +64 -3
  14. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/PKG-INFO +7 -2
  15. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/SOURCES.txt +1 -0
  16. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/requires.txt +1 -1
  17. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/pyproject.toml +2 -2
  18. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/__init__.py +0 -0
  19. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/__init__.py +0 -0
  20. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/agents/tools.py +0 -0
  21. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/__init__.py +0 -0
  22. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/deps.py +0 -0
  23. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/__init__.py +0 -0
  24. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/agents.py +0 -0
  25. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/assets.py +0 -0
  26. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/generate.py +0 -0
  27. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/models.py +0 -0
  28. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/preview.py +0 -0
  29. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/routes/projects.py +0 -0
  30. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/api/websocket.py +0 -0
  31. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/cli.py +0 -0
  32. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/config.py +0 -0
  33. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/__init__.py +0 -0
  34. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/asset_handler.py +0 -0
  35. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/gemini_patch.py +0 -0
  36. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/engine/project_manager.py +0 -0
  37. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/storage/__init__.py +0 -0
  38. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/storage/database.py +0 -0
  39. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite/storage/repository.py +0 -0
  40. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/dependency_links.txt +0 -0
  41. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/entry_points.txt +0 -0
  42. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/agentsite.egg-info/top_level.txt +0 -0
  43. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/setup.cfg +0 -0
  44. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_agents.py +0 -0
  45. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_api.py +0 -0
  46. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_models.py +0 -0
  47. {agentsite-0.0.3.dev1 → agentsite-0.0.4.dev1}/tests/test_project_manager.py +0 -0
  48. {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.dev1
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.40
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
  [![PyPI version](https://badge.fury.io/py/agentsite.svg)](https://badge.fury.io/py/agentsite)
33
33
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
34
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
35
+ [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://github.com/jhd3197/AgentSite/blob/main/Dockerfile)
35
36
  [![Built with Prompture](https://img.shields.io/badge/built%20with-Prompture-blueviolet)](https://pypi.org/project/prompture/)
36
37
 
38
+ [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/jhd3197/AgentSite)
39
+ [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/jhd3197/AgentSite)
40
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](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
  [![PyPI version](https://badge.fury.io/py/agentsite.svg)](https://badge.fury.io/py/agentsite)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
+ [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://github.com/jhd3197/AgentSite/blob/main/Dockerfile)
6
7
  [![Built with Prompture](https://img.shields.io/badge/built%20with-Prompture-blueviolet)](https://pypi.org/project/prompture/)
7
8
 
9
+ [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/jhd3197/AgentSite)
10
+ [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/jhd3197/AgentSite)
11
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](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 are an expert frontend developer. You build complete, production-ready "
42
- "web pages using semantic HTML5, modern CSS, and vanilla JavaScript.\n\n"
43
- "WORKFLOW you MUST follow this exact output format:\n"
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
- "<html>...</html>\n"
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
- "```css\n"
52
- "/* styles.css */\n"
53
- "body { ... }\n"
54
- "```\n\n"
55
- "```javascript\n"
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
- designer,
144
- "Design a visual style for this website:\n\n"
145
- "Site Plan: {site_plan}\n\n"
146
- "Logo URL: {logo_url}\n"
147
- "Icon URL: {icon_url}\n\n"
148
- "Create a cohesive color scheme, typography, and spacing system.",
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
- developer,
198
- "Build the website page based on this plan:\n\n"
199
- "Site Plan: {site_plan}\n\n"
200
- "Style Spec: {style_spec}\n\n"
201
- "Logo URL: {logo_url}\n"
202
- "Icon URL: {icon_url}\n\n"
203
- "Use the write_file tool to save each file. Generate complete, "
204
- "self-contained HTML with inline or linked CSS/JS.",
205
- ))
206
-
207
- return SequentialGroup(steps, callbacks=callbacks)
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** is needed when building a new site, changing branding/colors, "
18
- "or creating a new visual identity. Skip for content-only edits or bug fixes.\n"
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
- "Always include an index page as the first page.",
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. You build complete, production-ready "
61
- "web pages using semantic HTML5, modern CSS, and vanilla JavaScript.\n\n"
62
- "WORKFLOW you MUST follow this process:\n"
63
- "1. Use the `write_file` tool to write EACH file (index.html, styles.css, script.js, etc.)\n"
64
- "2. After writing ALL files, return a JSON summary listing the files you wrote.\n\n"
65
- "IMPORTANT: Do NOT put file contents in your final JSON response. "
66
- "Write all file contents using the `write_file` tool, then return only the file paths "
67
- "in your JSON summary.\n\n"
68
- "Requirements:\n"
69
- "- Write clean, semantic HTML with proper heading hierarchy\n"
70
- "- Use CSS custom properties for theming (colors, fonts, spacing)\n"
71
- "- Make pages fully responsive (mobile-first approach)\n"
72
- "- Include smooth transitions and subtle animations\n"
73
- "- Add proper meta tags, viewport settings, and favicon links\n"
74
- "- Use Google Fonts via CDN link\n"
75
- "- Write accessible markup (ARIA labels, alt text, focus styles)\n\n"
76
- "Generate complete, self-contained files. Every HTML page should be fully functional "
77
- "when opened directly in a browser."
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 JSON output.",
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
  )
@@ -19,4 +19,5 @@ def create_reviewer_agent(model: str) -> Agent:
19
19
  name="reviewer",
20
20
  description="Reviews generated code for quality and accessibility",
21
21
  output_key="review_feedback",
22
+ options={"max_tokens": 4096},
22
23
  )
@@ -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
- value = os.environ.get(env_key, "")
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 Gemini tool result format fix at import time
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
- self._emit("error", agent=agent_key, data={"message": str(exc)})
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, 'aggregate_usage'):
377
- if not hasattr(pm_result, 'aggregate_usage'):
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
- result = remaining_pipeline.run("")
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
- # Detect developer failure: Prompture's SequentialGroup catches
413
- # agent errors internally (doesn't re-raise), so check for a
414
- # failed result with no files and no developer output.
415
- _dev_failed = (
416
- not result.success
417
- or (
418
- not written_files
419
- and not self._developer_output_text
420
- and not self._developer_tool_calls
421
- )
422
- )
423
- if _dev_failed and not written_files:
424
- dev_errors = [
425
- e for e in getattr(result, "errors", [])
426
- if getattr(e, "agent_name", "") in ("developer", "agentsite_developer")
427
- ]
428
- logger.warning(
429
- "Developer pipeline produced no output (success=%s, errors=%d, "
430
- "written_files=%d, dev_output_len=%d, tool_calls=%d). "
431
- "Retrying with plain text developer.",
432
- result.success,
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 website page based on this plan:\n\n"
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
- "Generate complete, self-contained HTML with inline or linked CSS/JS."
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, 'aggregate_usage'):
467
- if not hasattr(result, 'aggregate_usage'):
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, 'aggregate_usage') else {}
476
- if hasattr(result, 'aggregate_usage'):
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
- "page_output_text[:500]: %s",
508
- project.id, slug, version_number,
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, len(source_text),
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, slug, version_number,
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("generation_complete", data={
566
- "success": result.success,
567
- "slug": slug,
568
- "version": version_number,
569
- "files": final_files,
570
- "files_content": files_content,
571
- "usage": combined_usage,
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("generation_complete", data={
583
- "success": False,
584
- "slug": slug,
585
- "version": version_number,
586
- "files": [],
587
- "error": str(exc),
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
- def _write_files_from_output(
613
- self, project_id: str, slug: str, version: int, output_text: str
614
- ) -> None:
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, output_text)
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, output_text)
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
- border_radius: str = Field(default="8px", description="Default border radius")
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(description="Event type: phase_start, phase_complete, agent_start, agent_complete, error, file_written, generation_complete")
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.dev1
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.40
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
  [![PyPI version](https://badge.fury.io/py/agentsite.svg)](https://badge.fury.io/py/agentsite)
33
33
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
34
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
35
+ [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://github.com/jhd3197/AgentSite/blob/main/Dockerfile)
35
36
  [![Built with Prompture](https://img.shields.io/badge/built%20with-Prompture-blueviolet)](https://pypi.org/project/prompture/)
36
37
 
38
+ [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/jhd3197/AgentSite)
39
+ [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/jhd3197/AgentSite)
40
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](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
@@ -1,4 +1,4 @@
1
- prompture>=0.0.40
1
+ prompture>=0.0.47
2
2
  fastapi>=0.100
3
3
  uvicorn[standard]>=0.20
4
4
  aiosqlite>=0.19
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentsite"
7
- version = "0.0.3.dev1"
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.40",
18
+ "prompture>=0.0.47",
19
19
  "fastapi>=0.100",
20
20
  "uvicorn[standard]>=0.20",
21
21
  "aiosqlite>=0.19",
File without changes