spec4 0.1.0__py3-none-any.whl
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.
- spec4/__init__.py +1 -0
- spec4/a2a_bus.py +60 -0
- spec4/agents/__init__.py +0 -0
- spec4/agents/brainstormer.py +258 -0
- spec4/agents/phaser.py +247 -0
- spec4/agents/reviewer.py +246 -0
- spec4/agents/stack_advisor.py +281 -0
- spec4/app.py +267 -0
- spec4/app_constants.py +48 -0
- spec4/assets/favicon.svg +5 -0
- spec4/assets/v3.css +270 -0
- spec4/browser_prefs.py +5 -0
- spec4/callbacks.py +713 -0
- spec4/layouts.py +788 -0
- spec4/project_manager.py +137 -0
- spec4/providers.py +101 -0
- spec4/py.typed +0 -0
- spec4/session.py +137 -0
- spec4/tavily_mcp.py +195 -0
- spec4-0.1.0.dist-info/METADATA +179 -0
- spec4-0.1.0.dist-info/RECORD +23 -0
- spec4-0.1.0.dist-info/WHEEL +4 -0
- spec4-0.1.0.dist-info/entry_points.txt +3 -0
spec4/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
spec4/a2a_bus.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from a2a.types import (
|
|
6
|
+
Message,
|
|
7
|
+
Part,
|
|
8
|
+
Role,
|
|
9
|
+
Task,
|
|
10
|
+
TaskState,
|
|
11
|
+
TaskStatus,
|
|
12
|
+
TextPart,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def make_message(role: Role, text: str, task_id: str | None = None, context_id: str | None = None) -> Message:
|
|
17
|
+
"""Create an A2A Message with a single TextPart."""
|
|
18
|
+
return Message(
|
|
19
|
+
role=role,
|
|
20
|
+
message_id=str(uuid.uuid4()),
|
|
21
|
+
parts=[Part(root=TextPart(text=text))],
|
|
22
|
+
task_id=task_id,
|
|
23
|
+
context_id=context_id,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_task(context_id: str, initial_message: Message, session: dict) -> Task:
|
|
28
|
+
"""Create a new A2A Task in submitted state and store it in session."""
|
|
29
|
+
task_id = str(uuid.uuid4())
|
|
30
|
+
msg = Message(
|
|
31
|
+
role=initial_message.role,
|
|
32
|
+
message_id=initial_message.message_id,
|
|
33
|
+
parts=initial_message.parts,
|
|
34
|
+
task_id=task_id,
|
|
35
|
+
context_id=context_id,
|
|
36
|
+
)
|
|
37
|
+
task = Task(
|
|
38
|
+
id=task_id,
|
|
39
|
+
context_id=context_id,
|
|
40
|
+
status=TaskStatus(state=TaskState.submitted),
|
|
41
|
+
history=[msg],
|
|
42
|
+
)
|
|
43
|
+
session["a2a_tasks"][task_id] = task
|
|
44
|
+
return task
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def update_task(task_id: str, state: TaskState, message: Message | None = None, session: dict = None) -> Task:
|
|
48
|
+
"""Update a task's state and optionally append a message to its history."""
|
|
49
|
+
task: Task = session["a2a_tasks"][task_id]
|
|
50
|
+
new_history = list(task.history or [])
|
|
51
|
+
if message is not None:
|
|
52
|
+
new_history.append(message)
|
|
53
|
+
updated = task.model_copy(
|
|
54
|
+
update={
|
|
55
|
+
"status": TaskStatus(state=state, message=message),
|
|
56
|
+
"history": new_history,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
session["a2a_tasks"][task_id] = updated
|
|
60
|
+
return updated
|
spec4/agents/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
|
|
7
|
+
from spec4 import tavily_mcp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SYSTEM_PROMPT = """\
|
|
11
|
+
You are an experienced agentic AI application developer and user experience (UX) professional. \
|
|
12
|
+
The user has an initial idea for a new software project, but the idea needs to be further \
|
|
13
|
+
developed and refined, perhaps to a large degree. Your job is to collaborate with the user \
|
|
14
|
+
to brainstorm on the idea and create a vision statement, suggesting ideas, alternatives, and \
|
|
15
|
+
questions. You will also try to identify any gaps in the vision which will make it unclear, \
|
|
16
|
+
and make the user aware of them. The vision statement produced here will be consumed by the \
|
|
17
|
+
StackAdvisor agent to guide technology stack selection and by the Phaser agent to plan \
|
|
18
|
+
implementation phases, so clarity and completeness directly influence the quality of those \
|
|
19
|
+
downstream stages.
|
|
20
|
+
|
|
21
|
+
You will lead the user through a series of questions ONE AT A TIME, with a goal of reaching a \
|
|
22
|
+
concrete, well-defined vision. Ask only one question per response — never ask multiple questions \
|
|
23
|
+
at once. Wait for the user's answer before moving to the next question. For each question you \
|
|
24
|
+
will offer a selection of numbered options, always including the option for the user to suggest \
|
|
25
|
+
their own option. When options are mutually exclusive, explicitly tell the user to pick one. \
|
|
26
|
+
When multiple options can be combined, explicitly tell the user they can select as many as they \
|
|
27
|
+
like (e.g., "Pick one or more — you can combine them"). When asking a yes/no confirmation \
|
|
28
|
+
question, end it with "(yes/no)". When presenting a numbered list where the user picks exactly \
|
|
29
|
+
one, end with "Please select an option (answer with number and/or optional comments)". When presenting a numbered list \
|
|
30
|
+
where multiple selections are allowed, end with "(answer with number(s) and/or optional \
|
|
31
|
+
comments)". As you go through and answer the series \
|
|
32
|
+
of questions you will add to the overall vision statement, reviewing it with the user at each \
|
|
33
|
+
step as you progress, and allowing them to return to a previous choice and change it. Any links \
|
|
34
|
+
in your responses to the user should open a new browser tab.
|
|
35
|
+
|
|
36
|
+
Whenever the user mentions a technical standard, specification, protocol, API, or SDK \
|
|
37
|
+
(for example "the MCP protocol", "the OpenAI API", "the A2A protocol", "OAuth 2.0"), use the \
|
|
38
|
+
web_search tool to find the canonical documentation URL. Present your findings and ask the \
|
|
39
|
+
user to confirm you have identified the correct standard before continuing. Once confirmed, \
|
|
40
|
+
add the standard and its canonical URL to the `references` array in the vision statement JSON. \
|
|
41
|
+
If the reference cannot be found via web search or appears to be specific to the user or \
|
|
42
|
+
project, label it as "unique to this project" rather than guessing. Every technical standard, \
|
|
43
|
+
specification, protocol, API, or SDK mentioned anywhere in the vision statement must appear in \
|
|
44
|
+
`references`.
|
|
45
|
+
|
|
46
|
+
When a code review of an existing project is provided at the start of the conversation, use it \
|
|
47
|
+
to inform your understanding of what the project currently does, and focus on helping the user \
|
|
48
|
+
articulate the project's purpose, audience, and goals as a vision statement.
|
|
49
|
+
|
|
50
|
+
You will not write code, select an implementation approach, or ask about technical infrastructure, \
|
|
51
|
+
technology stack, hosting, deployment, or software libraries — those topics are handled by a \
|
|
52
|
+
separate agent. Focus exclusively on what the software does, who it is for, and why it matters. \
|
|
53
|
+
When you think that the vision is potentially complete you will ask the user if they agree that \
|
|
54
|
+
it is complete and should be finalized. When the user determines that the software project \
|
|
55
|
+
vision is complete you will generate a vision statement as a fenced JSON code block. \
|
|
56
|
+
Here is an example:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"vision_statement": {
|
|
61
|
+
"name": "BiteGuide",
|
|
62
|
+
"vision": {
|
|
63
|
+
"purpose": "A **smart restaurant discovery app** that combines **AI-powered recommendations** \
|
|
64
|
+
with **user-driven reviews**, helping **food enthusiasts, casual diners, and travelers** \
|
|
65
|
+
find personalized dining experiences tailored to their preferences and context.",
|
|
66
|
+
"target_audience": [
|
|
67
|
+
"Food enthusiasts seeking hidden gems and trending spots",
|
|
68
|
+
"Casual diners looking for reliable, everyday options",
|
|
69
|
+
"Travelers exploring local favorites in new cities"
|
|
70
|
+
],
|
|
71
|
+
"key_features_mvp": [
|
|
72
|
+
{
|
|
73
|
+
"AI_Recommendations": {
|
|
74
|
+
"description": "Personalized suggestions based on user preferences, past visits, and \
|
|
75
|
+
context (e.g., time of day, location, mood).",
|
|
76
|
+
"example": "\"Since you loved the spicy noodles last time, here's a new Sichuan spot \
|
|
77
|
+
nearby.\""
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"User_Reviews": {
|
|
82
|
+
"description": "Verified user-generated reviews, photos, and ratings with tags (e.g., \
|
|
83
|
+
'vegan-friendly,' 'great for groups').",
|
|
84
|
+
"example": "\"See what real diners say—no fake reviews here.\""
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"Restaurant_Profiles": {
|
|
89
|
+
"description": "Detailed pages with menus, hours, photos, and user reviews.",
|
|
90
|
+
"example": "\"Browse the full menu and see photos of every dish before you go.\""
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"Search_Filters": {
|
|
95
|
+
"description": "Search by cuisine, price, dietary needs, mood (e.g., 'romantic,' \
|
|
96
|
+
'family-friendly'), or proximity.",
|
|
97
|
+
"example": "\"Find a gluten-free Italian restaurant within 10 minutes.\""
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"differentiators": [
|
|
102
|
+
"AI that adapts to **user habits, mood, and real-time context** (e.g., weather, social \
|
|
103
|
+
circle)—not just generic recommendations."
|
|
104
|
+
],
|
|
105
|
+
"monetization": {
|
|
106
|
+
"current": "Free tier only (MVP). Revenue models like premium subscriptions, restaurant \
|
|
107
|
+
partnerships, or ads will be explored post-launch based on user feedback.",
|
|
108
|
+
"future_options": [
|
|
109
|
+
"Freemium upgrades (e.g., advanced AI, ad-free experience)",
|
|
110
|
+
"Restaurant partnerships (e.g., featured listings, commissions)",
|
|
111
|
+
"Affiliate links (e.g., delivery services, reservation platforms)"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"future_enhancements": [
|
|
115
|
+
{
|
|
116
|
+
"Advanced_AI": {
|
|
117
|
+
"description": "Predictive suggestions (e.g., \"You'll probably love this new opening \
|
|
118
|
+
based on your trends\").",
|
|
119
|
+
"example": "AI anticipates user preferences before they search."
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"references": [
|
|
124
|
+
{
|
|
125
|
+
"standard": "OAuth 2.0",
|
|
126
|
+
"url": "https://oauth.net/2/"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
You will ONLY include the vision selections that the user has made in the vision statement. You \
|
|
135
|
+
will not add anything that the user has not explicitly selected. You will double-check and validate \
|
|
136
|
+
that the JSON in the vision statement is complete, valid, and legal.
|
|
137
|
+
|
|
138
|
+
Output only the JSON code block when generating the final vision statement — no additional text after it.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _extract_vision_json(text: str) -> dict | None:
|
|
143
|
+
"""Extract a JSON vision statement from a fenced code block in the LLM response."""
|
|
144
|
+
match = re.search(r"```json\s*(\{.*\})\s*```", text, re.DOTALL)
|
|
145
|
+
if match:
|
|
146
|
+
try:
|
|
147
|
+
data = json.loads(match.group(1))
|
|
148
|
+
if "vision_statement" in data:
|
|
149
|
+
return data
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
pass
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run(
|
|
156
|
+
user_input: str | None,
|
|
157
|
+
session: dict,
|
|
158
|
+
llm_config: dict,
|
|
159
|
+
) -> Generator[str, None, None]:
|
|
160
|
+
"""Brainstormer — collaborates with the user to develop a software project vision.
|
|
161
|
+
|
|
162
|
+
Yields text chunks suitable for consumption by st.write_stream().
|
|
163
|
+
Mutates `session` to track conversation state and vision output.
|
|
164
|
+
"""
|
|
165
|
+
if "brainstormer_messages" not in session:
|
|
166
|
+
session["brainstormer_messages"] = []
|
|
167
|
+
|
|
168
|
+
msgs = session["brainstormer_messages"]
|
|
169
|
+
|
|
170
|
+
if user_input is None:
|
|
171
|
+
if msgs:
|
|
172
|
+
# Re-entry: replay last assistant response without calling LLM
|
|
173
|
+
for msg in reversed(msgs):
|
|
174
|
+
if msg["role"] == "assistant":
|
|
175
|
+
yield msg["content"]
|
|
176
|
+
return
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
vision = session.get("vision_statement")
|
|
180
|
+
specmem = session.get("specmem")
|
|
181
|
+
code_review = session.get("code_review")
|
|
182
|
+
|
|
183
|
+
code_review_block = (
|
|
184
|
+
f"\n\nFor context, here is a code review of the existing project:\n\n"
|
|
185
|
+
f"```json\n{json.dumps(code_review, indent=2)}\n```\n"
|
|
186
|
+
if code_review else ""
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if vision:
|
|
190
|
+
# Pre-loaded vision: ask user to continue with it or start fresh
|
|
191
|
+
vision_text = json.dumps(vision, indent=2)
|
|
192
|
+
msgs.append({
|
|
193
|
+
"role": "user",
|
|
194
|
+
"content": (
|
|
195
|
+
f"I have an existing vision statement:{code_review_block}\n\n"
|
|
196
|
+
f"```json\n{vision_text}\n```\n\n"
|
|
197
|
+
"Please introduce yourself as Brainstormer and briefly summarize this vision. "
|
|
198
|
+
"Then ask me: would I like to **continue refining this existing vision**, "
|
|
199
|
+
"or would I prefer to **start a completely new vision** from scratch? "
|
|
200
|
+
"Wait for my answer before proceeding."
|
|
201
|
+
),
|
|
202
|
+
})
|
|
203
|
+
# Fall through to LLM call below
|
|
204
|
+
elif code_review:
|
|
205
|
+
# Existing project with code review but no vision yet
|
|
206
|
+
msgs.append({
|
|
207
|
+
"role": "user",
|
|
208
|
+
"content": (
|
|
209
|
+
"I have an existing software project that I'd like to create a vision "
|
|
210
|
+
"statement for. Here is a code review of the existing project:\n\n"
|
|
211
|
+
f"```json\n{json.dumps(code_review, indent=2)}\n```\n\n"
|
|
212
|
+
+ (f"Additional project notes:\n\n{specmem}\n\n" if specmem else "")
|
|
213
|
+
+ "Please introduce yourself as Brainstormer. Briefly describe what you "
|
|
214
|
+
"understand about this project from the code review, then begin your "
|
|
215
|
+
"usual question-by-question process to develop the vision statement. "
|
|
216
|
+
"Use the code review as context to inform your questions."
|
|
217
|
+
),
|
|
218
|
+
})
|
|
219
|
+
# Fall through to LLM call below
|
|
220
|
+
elif specmem:
|
|
221
|
+
# Existing project notes but no vision yet
|
|
222
|
+
msgs.append({
|
|
223
|
+
"role": "user",
|
|
224
|
+
"content": (
|
|
225
|
+
"I have an existing software project that I'd like to create a vision "
|
|
226
|
+
"statement for. Here is a summary of the current project state:\n\n"
|
|
227
|
+
f"{specmem}\n\n"
|
|
228
|
+
"Please introduce yourself as Brainstormer. Briefly describe what you "
|
|
229
|
+
"understand about this project from the summary, then begin your "
|
|
230
|
+
"usual question-by-question process to develop the vision statement. "
|
|
231
|
+
"Use the summary as context to inform your questions."
|
|
232
|
+
),
|
|
233
|
+
})
|
|
234
|
+
# Fall through to LLM call below
|
|
235
|
+
else:
|
|
236
|
+
# Fresh start: static greeting
|
|
237
|
+
yield (
|
|
238
|
+
"Hello! I'm the **Brainstormer**. I'll help you develop a clear, "
|
|
239
|
+
"well-defined vision for your software project.\n\n"
|
|
240
|
+
"What's your initial idea for the project? It can be rough — "
|
|
241
|
+
"we'll refine it together."
|
|
242
|
+
)
|
|
243
|
+
return
|
|
244
|
+
else:
|
|
245
|
+
msgs.append({"role": "user", "content": user_input})
|
|
246
|
+
|
|
247
|
+
# Build system prompt, adding web search note when Tavily is configured.
|
|
248
|
+
tavily_api_key = session.get("tavily_api_key")
|
|
249
|
+
system = SYSTEM_PROMPT + (tavily_mcp.WEB_SEARCH_ADDENDUM if tavily_api_key else "")
|
|
250
|
+
|
|
251
|
+
yield from tavily_mcp.stream_turn(system, msgs, llm_config, tavily_api_key)
|
|
252
|
+
|
|
253
|
+
# Detect if the LLM generated a final vision JSON (last assistant message).
|
|
254
|
+
full_text = next((m["content"] or "" for m in reversed(msgs) if m["role"] == "assistant"), "")
|
|
255
|
+
vision = _extract_vision_json(full_text)
|
|
256
|
+
if vision:
|
|
257
|
+
session["brainstormer_state"] = "vision_complete"
|
|
258
|
+
session["vision_statement"] = vision
|
spec4/agents/phaser.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
|
|
7
|
+
from spec4 import tavily_mcp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SYSTEM_PROMPT = """\
|
|
11
|
+
Role: You are Phaser, an expert AI Software Architect specialized in Incremental Delivery \
|
|
12
|
+
Strategy. Your goal is to take a high-level software vision and a specific tech stack, then \
|
|
13
|
+
decompose them into a sequence of high-probability, executable development phases.
|
|
14
|
+
|
|
15
|
+
Objective:
|
|
16
|
+
|
|
17
|
+
Break down complex projects into N modular phases. Each phase must be designed such that an AI \
|
|
18
|
+
coding agent (like Claude Code) can implement it with complete success on the first attempt. You \
|
|
19
|
+
prioritize stability, testing foundations, and "vertical slices" of functionality over broad, \
|
|
20
|
+
unimplemented scaffolding.
|
|
21
|
+
|
|
22
|
+
Phase 1 Strategy: The Steel Thread
|
|
23
|
+
|
|
24
|
+
* Mandatory "Hello World": Phase 1 must always be a "Steel Thread"—a minimal, functional \
|
|
25
|
+
end-to-end path that validates the core "plumbing" of the tech stack.
|
|
26
|
+
* Connectivity First: Focus on connecting primary layers (e.g., Frontend to Backend, or Backend \
|
|
27
|
+
to Database).
|
|
28
|
+
* Fail-Fast Logic: If the plumbing (env vars, DB connections, API handshakes) doesn't work in \
|
|
29
|
+
Phase 1, everything else will fail. Do not move to feature development until the core architecture \
|
|
30
|
+
is proven "alive."
|
|
31
|
+
|
|
32
|
+
Stack Spec Fidelity:
|
|
33
|
+
|
|
34
|
+
You must treat the stack spec as the authoritative list of approved system components, infrastructure, \
|
|
35
|
+
and library dependencies. If you determine that a phase requires any component, database, service, \
|
|
36
|
+
or library dependency that is NOT already defined in the stack spec, you must stop and ask the user \
|
|
37
|
+
for explicit confirmation before including it. Describe why it is needed and what it would add, \
|
|
38
|
+
then ask "(yes/no)" and wait for the user's approval. Do not assume approval — only add it to a phase after the user confirms.
|
|
39
|
+
|
|
40
|
+
Phasing Logic & Constraints:
|
|
41
|
+
|
|
42
|
+
* Success-First Design: If a phase is too large (e.g., "Build the entire Auth system"), break it \
|
|
43
|
+
down further (e.g., "Phase 2: Database Schema & Migration").
|
|
44
|
+
* Strict Scoping: Each phase's documentation must only contain requirements for that specific \
|
|
45
|
+
phase. Do not distract the implementer with future-phase requirements.
|
|
46
|
+
* Cumulative Progress: Phase N must build directly upon the code produced in Phase N-1.
|
|
47
|
+
* Verification: Every phase must include a "Verification" section with the exact command or \
|
|
48
|
+
criteria to prove completion.
|
|
49
|
+
|
|
50
|
+
Output Format:
|
|
51
|
+
|
|
52
|
+
You will output a series of JSON objects, one for each phase. Each object must follow this schema:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
57
|
+
"type": "object",
|
|
58
|
+
"properties": {
|
|
59
|
+
"phase_number": { "type": "integer" },
|
|
60
|
+
"total_phases": { "type": "integer" },
|
|
61
|
+
"phase_title": { "type": "string" },
|
|
62
|
+
"phase_summary": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "What this phase achieves and why, scoped to this phase only."
|
|
65
|
+
},
|
|
66
|
+
"tech_stack_spec": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"dependencies": { "type": "array", "items": { "type": "string" } },
|
|
70
|
+
"configurations": { "type": "string", "description": "Env vars, ports, or config files needed." }
|
|
71
|
+
},
|
|
72
|
+
"required": ["dependencies", "configurations"]
|
|
73
|
+
},
|
|
74
|
+
"instructions": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": { "type": "string" },
|
|
77
|
+
"description": "Step-by-step technical instructions for the AI coder."
|
|
78
|
+
},
|
|
79
|
+
"risk_assessment": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"potential_bottlenecks": { "type": "string" },
|
|
83
|
+
"mitigation_strategy": { "type": "string" }
|
|
84
|
+
},
|
|
85
|
+
"required": ["potential_bottlenecks", "mitigation_strategy"]
|
|
86
|
+
},
|
|
87
|
+
"verification": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "The exact command or criteria to verify this phase succeeded."
|
|
90
|
+
},
|
|
91
|
+
"references": {
|
|
92
|
+
"type": "array",
|
|
93
|
+
"items": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"standard": { "type": "string" },
|
|
97
|
+
"url": { "type": "string" }
|
|
98
|
+
},
|
|
99
|
+
"required": ["standard", "url"]
|
|
100
|
+
},
|
|
101
|
+
"description": "Canonical links for every technical standard, specification, protocol, API, or SDK used in this phase. Use an empty array if none apply."
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"required": [
|
|
105
|
+
"phase_number",
|
|
106
|
+
"total_phases",
|
|
107
|
+
"phase_title",
|
|
108
|
+
"phase_summary",
|
|
109
|
+
"tech_stack_spec",
|
|
110
|
+
"instructions",
|
|
111
|
+
"risk_assessment",
|
|
112
|
+
"verification"
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Here is a concrete example of a single phase object:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"phase_number": 1,
|
|
122
|
+
"total_phases": 4,
|
|
123
|
+
"phase_title": "Steel Thread — API Health Check & Database Connection",
|
|
124
|
+
"phase_summary": "Establish a live end-to-end connection from the FastAPI backend to the PostgreSQL database. A single health-check endpoint confirms the stack is wired together before any feature development begins.",
|
|
125
|
+
"tech_stack_spec": {
|
|
126
|
+
"dependencies": ["fastapi", "uvicorn", "sqlalchemy", "psycopg2-binary", "pydantic"],
|
|
127
|
+
"configurations": "DATABASE_URL env var (e.g. postgresql://user:pass@localhost/biteguide); API listens on PORT 8000"
|
|
128
|
+
},
|
|
129
|
+
"instructions": [
|
|
130
|
+
"Initialise the FastAPI app in main.py with a single GET /health endpoint.",
|
|
131
|
+
"Configure SQLAlchemy with the DATABASE_URL env var and open the connection on startup.",
|
|
132
|
+
"Add a startup event that runs SELECT 1 to verify the database is reachable.",
|
|
133
|
+
"Return {\"status\": \"ok\", \"db\": \"connected\"} from /health on success."
|
|
134
|
+
],
|
|
135
|
+
"risk_assessment": {
|
|
136
|
+
"potential_bottlenecks": "Missing or malformed DATABASE_URL will cause a silent import error rather than a clear startup failure.",
|
|
137
|
+
"mitigation_strategy": "Wrap the startup DB check in a try/except and raise a descriptive RuntimeError if the connection fails, so the problem is immediately visible in logs."
|
|
138
|
+
},
|
|
139
|
+
"verification": "Run `uvicorn main:app --reload` and call GET http://localhost:8000/health — expect HTTP 200 with {\"status\": \"ok\", \"db\": \"connected\"}.",
|
|
140
|
+
"references": [
|
|
141
|
+
{"standard": "FastAPI", "url": "https://fastapi.tiangolo.com/"},
|
|
142
|
+
{"standard": "SQLAlchemy", "url": "https://docs.sqlalchemy.org/"}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Technical Standards Identification:
|
|
148
|
+
|
|
149
|
+
Whenever the vision, stack, or user refers to a technical standard, specification, protocol, \
|
|
150
|
+
API, or SDK, use the web_search tool to find the canonical documentation URL. Ask the user \
|
|
151
|
+
to confirm you have identified the correct standard. Once confirmed, add the standard and its \
|
|
152
|
+
canonical URL to the `references` array in every phase JSON object that uses it. If the \
|
|
153
|
+
reference cannot be found via web search or appears to be specific to the user or project, \
|
|
154
|
+
label it as "unique to this project" rather than guessing. Every technical standard, \
|
|
155
|
+
specification, protocol, API, or SDK referenced in a phase must appear in that phase's \
|
|
156
|
+
`references` array.
|
|
157
|
+
|
|
158
|
+
Operating Procedure:
|
|
159
|
+
|
|
160
|
+
1. Analyze Complexity: Review the full Vision and Tech Stack.
|
|
161
|
+
2. Establish the Steel Thread: Identify the simplest "living" version of the app for Phase 1.
|
|
162
|
+
3. Determine N: Calculate the total number of phases needed to reach the final vision.
|
|
163
|
+
4. Identify Risks: For every phase, look for "hallucination traps" — areas where the AI might \
|
|
164
|
+
guess incorrectly (e.g., complex regex, tricky auth flows) and provide explicit guidance in the \
|
|
165
|
+
risk_assessment.
|
|
166
|
+
|
|
167
|
+
User Review and Output:
|
|
168
|
+
|
|
169
|
+
1. When the phases are defined, present them to the user in text and ask the user to review, \
|
|
170
|
+
describe edits, or approve them — end the question with "(yes/no)". Any links in your \
|
|
171
|
+
responses should open a new browser tab.
|
|
172
|
+
2. When the user has approved the phases, immediately output ALL phase JSON blocks in a \
|
|
173
|
+
single response — one fenced JSON code block per phase, in order. Do NOT announce that you \
|
|
174
|
+
are about to output them, do not say "I will now output", do not add any explanation \
|
|
175
|
+
before or between the blocks. Just output the JSON blocks directly, back to back. The \
|
|
176
|
+
application will automatically detect the JSON blocks, package them into a zip file in \
|
|
177
|
+
memory, and present a download button — you do not need to do anything else.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _extract_phases(text: str) -> list[dict]:
|
|
182
|
+
"""Extract all JSON phase objects from fenced code blocks in the LLM response."""
|
|
183
|
+
phases = []
|
|
184
|
+
for match in re.finditer(r"```json\s*(.*?)\s*```", text, re.DOTALL):
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(match.group(1))
|
|
187
|
+
if "phase_number" in data:
|
|
188
|
+
phases.append(data)
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
pass
|
|
191
|
+
return phases
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def run(
|
|
195
|
+
user_input: str | None,
|
|
196
|
+
session: dict,
|
|
197
|
+
llm_config: dict,
|
|
198
|
+
) -> Generator[str, None, None]:
|
|
199
|
+
"""Phaser — decomposes vision + stack into executable coding phases.
|
|
200
|
+
|
|
201
|
+
Yields text chunks suitable for consumption by st.write_stream().
|
|
202
|
+
Mutates `session` to track state.
|
|
203
|
+
"""
|
|
204
|
+
if "phaser_messages" not in session:
|
|
205
|
+
session["phaser_messages"] = []
|
|
206
|
+
|
|
207
|
+
messages = session["phaser_messages"]
|
|
208
|
+
|
|
209
|
+
if user_input is None:
|
|
210
|
+
if messages:
|
|
211
|
+
# Re-entry: replay last assistant response without calling LLM
|
|
212
|
+
for msg in reversed(messages):
|
|
213
|
+
if msg["role"] == "assistant":
|
|
214
|
+
yield msg["content"]
|
|
215
|
+
return
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# Opening turn: seed with vision + stack, then call LLM
|
|
219
|
+
vision = session.get("vision_statement")
|
|
220
|
+
stack = session.get("stack_statement")
|
|
221
|
+
vision_block = (
|
|
222
|
+
f"Here is the project vision statement:\n\n```json\n{json.dumps(vision, indent=2)}\n```\n\n"
|
|
223
|
+
if vision else ""
|
|
224
|
+
)
|
|
225
|
+
stack_block = (
|
|
226
|
+
f"Here is the technology stack spec:\n\n```json\n{json.dumps(stack, indent=2)}\n```\n\n"
|
|
227
|
+
if stack else ""
|
|
228
|
+
)
|
|
229
|
+
seed = (
|
|
230
|
+
f"{vision_block}"
|
|
231
|
+
f"{stack_block}"
|
|
232
|
+
"Please analyze the vision and stack, then generate the full set of development phases."
|
|
233
|
+
)
|
|
234
|
+
messages.append({"role": "user", "content": seed})
|
|
235
|
+
else:
|
|
236
|
+
messages.append({"role": "user", "content": user_input})
|
|
237
|
+
|
|
238
|
+
tavily_api_key = session.get("tavily_api_key")
|
|
239
|
+
system = SYSTEM_PROMPT + (tavily_mcp.WEB_SEARCH_ADDENDUM if tavily_api_key else "")
|
|
240
|
+
|
|
241
|
+
yield from tavily_mcp.stream_turn(system, messages, llm_config, tavily_api_key)
|
|
242
|
+
|
|
243
|
+
full_text = next((m["content"] or "" for m in reversed(messages) if m["role"] == "assistant"), "")
|
|
244
|
+
phases = _extract_phases(full_text)
|
|
245
|
+
if phases:
|
|
246
|
+
session["phaser_state"] = "phases_complete"
|
|
247
|
+
session["phases"] = phases
|