Spaces:
Sleeping
Sleeping
firepenguindisopanda commited on
Commit ·
5e8f51e
1
Parent(s): 7e7f504
Refactor code structure for improved readability and maintainability
Browse files- .python-version +1 -0
- app/__init__.py +1 -0
- app/core/agents.py +148 -11
- app/core/cache.py +66 -46
- app/core/cost_control.py +138 -34
- app/core/database.py +141 -12
- app/core/models.py +85 -18
- app/core/observability.py +106 -50
- app/core/orchestrator.py +333 -150
- app/core/rag.py +1 -1
- app/core/resilience.py +26 -13
- app/main.py +34 -28
- app/prompts/_archive/prd_collector.md +53 -0
- app/prompts/{reviewer.md → _archive/reviewer.md} +0 -0
- app/prompts/api_designer.md +124 -60
- app/prompts/business_analyst.md +108 -48
- app/prompts/data_architect.md +89 -31
- app/prompts/devops_architect.md +124 -51
- app/prompts/environment_engineer.md +141 -41
- app/prompts/judges/{developer_judge.md → _archive/developer_judge.md} +0 -0
- app/prompts/judges/{analyst_judge.md → business_analyst_judge.md} +15 -26
- app/prompts/judges/product_owner_judge.md +10 -21
- app/prompts/judges/{architect_judge.md → solution_architect_judge.md} +17 -28
- app/prompts/oracle.md +79 -0
- app/prompts/product_owner.md +103 -70
- app/prompts/project_refiner.md +74 -31
- app/prompts/qa_strategist.md +102 -35
- app/prompts/security_analyst.md +115 -35
- app/prompts/solution_architect.md +99 -43
- app/prompts/spec_coordinator.md +80 -38
- app/prompts/technical_writer.md +123 -47
- app/prompts/ux_designer.md +106 -44
- app/routers/oracle.py +78 -0
- app/routers/prd.py +966 -0
- app/routers/web.py +2 -2
- pyproject.toml +0 -4
- requirements.txt +3 -81
- tests/test_prd_endpoints.py +94 -0
- tests/test_rag.py +20 -14
- uv.lock +0 -499
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Application package marker."""
|
app/core/agents.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
|
|
|
|
|
| 1 |
import logging
|
|
|
|
| 2 |
from collections.abc import AsyncIterator
|
| 3 |
from typing import Any
|
| 4 |
|
|
@@ -38,8 +41,43 @@ def _sanitize_json_str(raw: str) -> str:
|
|
| 38 |
def extract_json_from_response(content: str) -> dict[str, Any] | None:
|
| 39 |
"""
|
| 40 |
Extract JSON block from agent response content with light sanitization.
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
class AgentSystem:
|
|
@@ -175,7 +213,7 @@ paused requests to prevent further problems.
|
|
| 175 |
# Track token usage (estimate)
|
| 176 |
await self.token_tracker.record_usage(
|
| 177 |
prompt_tokens=0, # Hard to calculate without formatting
|
| 178 |
-
completion_tokens=self._estimate_tokens(response),
|
| 179 |
model="nvidia-nim", # Generic for now
|
| 180 |
operation=f"agent_{role}",
|
| 181 |
)
|
|
@@ -183,7 +221,7 @@ paused requests to prevent further problems.
|
|
| 183 |
# Mark provider as successful
|
| 184 |
await self.provider_manager.mark_success("nvidia")
|
| 185 |
|
| 186 |
-
return response
|
| 187 |
|
| 188 |
except CircuitOpenError:
|
| 189 |
logger.warning(f"Circuit open for {role}, attempting graceful degradation")
|
|
@@ -376,6 +414,14 @@ paused requests to prevent further problems.
|
|
| 376 |
"""
|
| 377 |
Process a step with streaming output for real-time UI updates.
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
Yields chunks of content as they are generated.
|
| 380 |
|
| 381 |
Args:
|
|
@@ -392,6 +438,32 @@ paused requests to prevent further problems.
|
|
| 392 |
>>> async for chunk in agent.process_step_streaming(role, context, []):
|
| 393 |
... print(chunk, end="", flush=True)
|
| 394 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
# Get streaming LLM
|
| 396 |
llm = self._get_llm_for_role(role, streaming=True)
|
| 397 |
|
|
@@ -429,11 +501,76 @@ paused requests to prevent further problems.
|
|
| 429 |
|
| 430 |
logger.info(f"Starting streaming for role={role.value}")
|
| 431 |
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
import logging
|
| 4 |
+
import re
|
| 5 |
from collections.abc import AsyncIterator
|
| 6 |
from typing import Any
|
| 7 |
|
|
|
|
| 41 |
def extract_json_from_response(content: str) -> dict[str, Any] | None:
|
| 42 |
"""
|
| 43 |
Extract JSON block from agent response content with light sanitization.
|
| 44 |
+
|
| 45 |
+
Strategy (tried in order):
|
| 46 |
+
1. Fenced code block: ```json ... ```
|
| 47 |
+
2. Raw JSON object: outermost { ... }
|
| 48 |
+
|
| 49 |
+
Returns parsed dict on success, None if no valid JSON found.
|
| 50 |
"""
|
| 51 |
+
if not content or not content.strip():
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
# --- Strategy 1: fenced ```json ... ``` blocks ---
|
| 55 |
+
json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", content)
|
| 56 |
+
if json_match:
|
| 57 |
+
raw = json_match.group(1).strip()
|
| 58 |
+
sanitized = _sanitize_json_str(raw)
|
| 59 |
+
try:
|
| 60 |
+
parsed = json.loads(sanitized)
|
| 61 |
+
if isinstance(parsed, dict):
|
| 62 |
+
return parsed
|
| 63 |
+
except json.JSONDecodeError:
|
| 64 |
+
logger.debug(
|
| 65 |
+
"Fenced JSON block found but failed to parse after sanitization"
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# --- Strategy 2: raw JSON object { ... } ---
|
| 69 |
+
json_match = re.search(r"\{[\s\S]*\}", content)
|
| 70 |
+
if json_match:
|
| 71 |
+
raw = json_match.group(0).strip()
|
| 72 |
+
sanitized = _sanitize_json_str(raw)
|
| 73 |
+
try:
|
| 74 |
+
parsed = json.loads(sanitized)
|
| 75 |
+
if isinstance(parsed, dict):
|
| 76 |
+
return parsed
|
| 77 |
+
except json.JSONDecodeError:
|
| 78 |
+
logger.debug("Raw JSON object found but failed to parse after sanitization")
|
| 79 |
+
|
| 80 |
+
return None
|
| 81 |
|
| 82 |
|
| 83 |
class AgentSystem:
|
|
|
|
| 213 |
# Track token usage (estimate)
|
| 214 |
await self.token_tracker.record_usage(
|
| 215 |
prompt_tokens=0, # Hard to calculate without formatting
|
| 216 |
+
completion_tokens=self._estimate_tokens(str(response)),
|
| 217 |
model="nvidia-nim", # Generic for now
|
| 218 |
operation=f"agent_{role}",
|
| 219 |
)
|
|
|
|
| 221 |
# Mark provider as successful
|
| 222 |
await self.provider_manager.mark_success("nvidia")
|
| 223 |
|
| 224 |
+
return str(response)
|
| 225 |
|
| 226 |
except CircuitOpenError:
|
| 227 |
logger.warning(f"Circuit open for {role}, attempting graceful degradation")
|
|
|
|
| 414 |
"""
|
| 415 |
Process a step with streaming output for real-time UI updates.
|
| 416 |
|
| 417 |
+
Includes the same resilience patterns as the non-streaming path:
|
| 418 |
+
- Circuit breaker check (fail-fast if API is unhealthy)
|
| 419 |
+
- Budget check (prevent runaway costs)
|
| 420 |
+
- Retry with exponential backoff on transient failures
|
| 421 |
+
- Token usage tracking (estimated from streamed output)
|
| 422 |
+
- Provider health marking (success/failure)
|
| 423 |
+
- Performance timing
|
| 424 |
+
|
| 425 |
Yields chunks of content as they are generated.
|
| 426 |
|
| 427 |
Args:
|
|
|
|
| 438 |
>>> async for chunk in agent.process_step_streaming(role, context, []):
|
| 439 |
... print(chunk, end="", flush=True)
|
| 440 |
"""
|
| 441 |
+
import time as _time
|
| 442 |
+
|
| 443 |
+
from .resilience import get_circuit_breaker
|
| 444 |
+
|
| 445 |
+
start_time = _time.monotonic()
|
| 446 |
+
|
| 447 |
+
# --- Circuit breaker check (fail-fast) ---
|
| 448 |
+
circuit = get_circuit_breaker("nvidia_api")
|
| 449 |
+
if not await circuit.can_execute():
|
| 450 |
+
logger.warning(
|
| 451 |
+
f"Circuit open for streaming {role.value}, returning degraded response"
|
| 452 |
+
)
|
| 453 |
+
yield self._get_degraded_response(role.value)
|
| 454 |
+
return
|
| 455 |
+
|
| 456 |
+
# --- Budget check ---
|
| 457 |
+
try:
|
| 458 |
+
check_budget_or_raise(1000) # Placeholder estimate
|
| 459 |
+
except Exception as e:
|
| 460 |
+
logger.error(f"Budget exceeded for streaming {role.value}: {e}")
|
| 461 |
+
track_error(
|
| 462 |
+
e, context={"role": role.value, "operation": "streaming_budget"}
|
| 463 |
+
)
|
| 464 |
+
yield f"\n\n[Error: Budget limit exceeded. {e}]"
|
| 465 |
+
return
|
| 466 |
+
|
| 467 |
# Get streaming LLM
|
| 468 |
llm = self._get_llm_for_role(role, streaming=True)
|
| 469 |
|
|
|
|
| 501 |
|
| 502 |
logger.info(f"Starting streaming for role={role.value}")
|
| 503 |
|
| 504 |
+
# --- Retry loop with exponential backoff ---
|
| 505 |
+
max_retries = 3
|
| 506 |
+
base_delay = 2.0
|
| 507 |
+
|
| 508 |
+
for attempt in range(max_retries + 1):
|
| 509 |
+
collected_content = ""
|
| 510 |
+
stream_started = False
|
| 511 |
+
|
| 512 |
+
try:
|
| 513 |
+
async for chunk in llm.astream(messages):
|
| 514 |
+
if chunk.content:
|
| 515 |
+
stream_started = True
|
| 516 |
+
chunk_text = str(chunk.content)
|
| 517 |
+
collected_content += chunk_text
|
| 518 |
+
yield chunk_text
|
| 519 |
+
|
| 520 |
+
# --- Stream completed successfully ---
|
| 521 |
+
await circuit.record_success()
|
| 522 |
+
await self.provider_manager.mark_success("nvidia")
|
| 523 |
+
|
| 524 |
+
# Track token usage (estimate from collected output)
|
| 525 |
+
await self.token_tracker.record_usage(
|
| 526 |
+
prompt_tokens=0, # Hard to calculate without tokenizer
|
| 527 |
+
completion_tokens=self._estimate_tokens(collected_content),
|
| 528 |
+
model="nvidia-nim",
|
| 529 |
+
operation=f"agent_{role.value}_streaming",
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
elapsed = _time.monotonic() - start_time
|
| 533 |
+
logger.info(
|
| 534 |
+
f"Streaming completed for role={role.value} in {elapsed:.2f}s, "
|
| 535 |
+
f"~{self._estimate_tokens(collected_content)} tokens"
|
| 536 |
+
)
|
| 537 |
+
return # Success — exit the retry loop
|
| 538 |
+
|
| 539 |
+
except Exception as e:
|
| 540 |
+
await circuit.record_failure(e)
|
| 541 |
+
await self.provider_manager.mark_failure("nvidia", e)
|
| 542 |
+
track_error(
|
| 543 |
+
e,
|
| 544 |
+
context={
|
| 545 |
+
"role": role.value,
|
| 546 |
+
"operation": "streaming",
|
| 547 |
+
"attempt": attempt + 1,
|
| 548 |
+
},
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
if stream_started:
|
| 552 |
+
# We already yielded partial content — cannot retry transparently.
|
| 553 |
+
# Yield error marker and stop.
|
| 554 |
+
logger.error(
|
| 555 |
+
f"Streaming error for {role.value} after partial output "
|
| 556 |
+
f"(attempt {attempt + 1}): {e}"
|
| 557 |
+
)
|
| 558 |
+
yield f"\n\n[Error: Stream interrupted — {e}]"
|
| 559 |
+
return
|
| 560 |
+
|
| 561 |
+
if attempt < max_retries:
|
| 562 |
+
import random
|
| 563 |
+
|
| 564 |
+
delay = min(base_delay * (2.0**attempt), 60.0)
|
| 565 |
+
delay *= 0.5 + random.random() # jitter
|
| 566 |
+
logger.warning(
|
| 567 |
+
f"Streaming attempt {attempt + 1}/{max_retries} failed for "
|
| 568 |
+
f"{role.value}, retrying in {delay:.2f}s: {e}"
|
| 569 |
+
)
|
| 570 |
+
await asyncio.sleep(delay)
|
| 571 |
+
else:
|
| 572 |
+
logger.error(
|
| 573 |
+
f"All {max_retries + 1} streaming attempts failed for "
|
| 574 |
+
f"{role.value}: {e}"
|
| 575 |
+
)
|
| 576 |
+
yield self._get_degraded_response(role.value)
|
app/core/cache.py
CHANGED
|
@@ -8,6 +8,7 @@ Uses HTTP-based Redis client optimized for serverless environments.
|
|
| 8 |
import json
|
| 9 |
import logging
|
| 10 |
import os
|
|
|
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from datetime import UTC, datetime
|
| 13 |
from functools import wraps
|
|
@@ -23,12 +24,13 @@ logger = logging.getLogger(__name__)
|
|
| 23 |
@dataclass
|
| 24 |
class CacheConfig:
|
| 25 |
"""Cache configuration with sensible defaults."""
|
|
|
|
| 26 |
# TTL values in seconds
|
| 27 |
-
summary_ttl: int = 300
|
| 28 |
-
trends_ttl: int = 600
|
| 29 |
-
projects_ttl: int = 300
|
| 30 |
-
runs_ttl: int = 120
|
| 31 |
-
default_ttl: int = 300
|
| 32 |
|
| 33 |
# Cache key prefixes
|
| 34 |
prefix: str = "specsbeforecode:observability"
|
|
@@ -71,6 +73,9 @@ class UpstashRedisCache:
|
|
| 71 |
if self._sync_client is None:
|
| 72 |
try:
|
| 73 |
from upstash_redis import Redis
|
|
|
|
|
|
|
|
|
|
| 74 |
self._sync_client = Redis(url=self.url, token=self.token)
|
| 75 |
logger.info("Upstash Redis sync client initialized")
|
| 76 |
except Exception as e:
|
|
@@ -86,6 +91,9 @@ class UpstashRedisCache:
|
|
| 86 |
if self._async_client is None:
|
| 87 |
try:
|
| 88 |
from upstash_redis.asyncio import Redis
|
|
|
|
|
|
|
|
|
|
| 89 |
self._async_client = Redis(url=self.url, token=self.token)
|
| 90 |
logger.info("Upstash Redis async client initialized")
|
| 91 |
except Exception as e:
|
|
@@ -134,12 +142,7 @@ class UpstashRedisCache:
|
|
| 134 |
logger.warning(f"Cache get error for {key}: {e}")
|
| 135 |
return None
|
| 136 |
|
| 137 |
-
def set(
|
| 138 |
-
self,
|
| 139 |
-
key: str,
|
| 140 |
-
value: Any,
|
| 141 |
-
ttl: int | None = None
|
| 142 |
-
) -> bool:
|
| 143 |
"""Set value in cache with TTL (sync)."""
|
| 144 |
client = self._get_sync_client()
|
| 145 |
if not client:
|
|
@@ -203,12 +206,7 @@ class UpstashRedisCache:
|
|
| 203 |
logger.warning(f"Async cache get error for {key}: {e}")
|
| 204 |
return None
|
| 205 |
|
| 206 |
-
async def aset(
|
| 207 |
-
self,
|
| 208 |
-
key: str,
|
| 209 |
-
value: Any,
|
| 210 |
-
ttl: int | None = None
|
| 211 |
-
) -> bool:
|
| 212 |
"""Set value in cache with TTL (async)."""
|
| 213 |
client = self._get_async_client()
|
| 214 |
if not client:
|
|
@@ -254,14 +252,22 @@ class UpstashRedisCache:
|
|
| 254 |
logger.warning(f"Async cache exists error for {key}: {e}")
|
| 255 |
return False
|
| 256 |
|
| 257 |
-
def get_summary_key(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
"""Generate cache key for summary stats."""
|
| 259 |
parts = ["summary", user_id, start_date, end_date]
|
| 260 |
if project_id:
|
| 261 |
parts.append(project_id)
|
| 262 |
return ":".join(parts)
|
| 263 |
|
| 264 |
-
def get_trends_key(
|
|
|
|
|
|
|
| 265 |
"""Generate cache key for trends data."""
|
| 266 |
parts = ["trends", user_id, period]
|
| 267 |
if project_id:
|
|
@@ -282,7 +288,7 @@ class UpstashRedisCache:
|
|
| 282 |
start_date: str,
|
| 283 |
end_date: str,
|
| 284 |
data: dict[str, Any],
|
| 285 |
-
project_id: str | None = None
|
| 286 |
) -> bool:
|
| 287 |
"""Cache summary statistics."""
|
| 288 |
key = self.get_summary_key(user_id, start_date, end_date, project_id)
|
|
@@ -293,7 +299,7 @@ class UpstashRedisCache:
|
|
| 293 |
user_id: str,
|
| 294 |
start_date: str,
|
| 295 |
end_date: str,
|
| 296 |
-
project_id: str | None = None
|
| 297 |
) -> dict[str, Any] | None:
|
| 298 |
"""Get cached summary statistics."""
|
| 299 |
key = self.get_summary_key(user_id, start_date, end_date, project_id)
|
|
@@ -304,17 +310,14 @@ class UpstashRedisCache:
|
|
| 304 |
user_id: str,
|
| 305 |
period: str,
|
| 306 |
data: dict[str, Any],
|
| 307 |
-
project_id: str | None = None
|
| 308 |
) -> bool:
|
| 309 |
"""Cache trends data."""
|
| 310 |
key = self.get_trends_key(user_id, period, project_id)
|
| 311 |
return await self.aset(key, data, ttl=self.config.trends_ttl)
|
| 312 |
|
| 313 |
async def get_cached_trends(
|
| 314 |
-
self,
|
| 315 |
-
user_id: str,
|
| 316 |
-
period: str,
|
| 317 |
-
project_id: str | None = None
|
| 318 |
) -> dict[str, Any] | None:
|
| 319 |
"""Get cached trends data."""
|
| 320 |
key = self.get_trends_key(user_id, period, project_id)
|
|
@@ -340,12 +343,15 @@ class UpstashRedisCache:
|
|
| 340 |
|
| 341 |
|
| 342 |
def cached(
|
| 343 |
-
key_func:
|
| 344 |
ttl: int | None = None,
|
| 345 |
-
cache_instance: UpstashRedisCache | None = None
|
| 346 |
):
|
| 347 |
"""
|
| 348 |
-
Decorator for caching async function results.
|
|
|
|
|
|
|
|
|
|
| 349 |
|
| 350 |
Args:
|
| 351 |
key_func: Function that generates cache key from function arguments
|
|
@@ -361,31 +367,44 @@ def cached(
|
|
| 361 |
# expensive operation
|
| 362 |
return await fetch_from_langsmith(...)
|
| 363 |
"""
|
|
|
|
| 364 |
def decorator(func):
|
| 365 |
@wraps(func)
|
| 366 |
async def wrapper(*args, **kwargs):
|
| 367 |
cache = cache_instance or get_cache()
|
| 368 |
|
| 369 |
-
# Generate cache key
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
result = await func(*args, **kwargs)
|
| 384 |
-
|
|
|
|
| 385 |
if result is not None and cache_key:
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
return result
|
|
|
|
| 388 |
return wrapper
|
|
|
|
| 389 |
return decorator
|
| 390 |
|
| 391 |
|
|
@@ -419,7 +438,7 @@ async def check_cache_health() -> dict[str, Any]:
|
|
| 419 |
"configured": cache.is_configured,
|
| 420 |
"status": "unknown",
|
| 421 |
"latency_ms": None,
|
| 422 |
-
"error": None
|
| 423 |
}
|
| 424 |
|
| 425 |
if not cache.is_configured:
|
|
@@ -429,6 +448,7 @@ async def check_cache_health() -> dict[str, Any]:
|
|
| 429 |
|
| 430 |
try:
|
| 431 |
import time
|
|
|
|
| 432 |
start = time.time()
|
| 433 |
|
| 434 |
# Ping test with a simple set/get
|
|
|
|
| 8 |
import json
|
| 9 |
import logging
|
| 10 |
import os
|
| 11 |
+
from collections.abc import Callable
|
| 12 |
from dataclasses import dataclass
|
| 13 |
from datetime import UTC, datetime
|
| 14 |
from functools import wraps
|
|
|
|
| 24 |
@dataclass
|
| 25 |
class CacheConfig:
|
| 26 |
"""Cache configuration with sensible defaults."""
|
| 27 |
+
|
| 28 |
# TTL values in seconds
|
| 29 |
+
summary_ttl: int = 300 # 5 minutes for summary stats
|
| 30 |
+
trends_ttl: int = 600 # 10 minutes for trend data
|
| 31 |
+
projects_ttl: int = 300 # 5 minutes for project list
|
| 32 |
+
runs_ttl: int = 120 # 2 minutes for run details
|
| 33 |
+
default_ttl: int = 300 # 5 minutes default
|
| 34 |
|
| 35 |
# Cache key prefixes
|
| 36 |
prefix: str = "specsbeforecode:observability"
|
|
|
|
| 73 |
if self._sync_client is None:
|
| 74 |
try:
|
| 75 |
from upstash_redis import Redis
|
| 76 |
+
|
| 77 |
+
if not self.url or not self.token:
|
| 78 |
+
return None
|
| 79 |
self._sync_client = Redis(url=self.url, token=self.token)
|
| 80 |
logger.info("Upstash Redis sync client initialized")
|
| 81 |
except Exception as e:
|
|
|
|
| 91 |
if self._async_client is None:
|
| 92 |
try:
|
| 93 |
from upstash_redis.asyncio import Redis
|
| 94 |
+
|
| 95 |
+
if not self.url or not self.token:
|
| 96 |
+
return None
|
| 97 |
self._async_client = Redis(url=self.url, token=self.token)
|
| 98 |
logger.info("Upstash Redis async client initialized")
|
| 99 |
except Exception as e:
|
|
|
|
| 142 |
logger.warning(f"Cache get error for {key}: {e}")
|
| 143 |
return None
|
| 144 |
|
| 145 |
+
def set(self, key: str, value: Any, ttl: int | None = None) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
"""Set value in cache with TTL (sync)."""
|
| 147 |
client = self._get_sync_client()
|
| 148 |
if not client:
|
|
|
|
| 206 |
logger.warning(f"Async cache get error for {key}: {e}")
|
| 207 |
return None
|
| 208 |
|
| 209 |
+
async def aset(self, key: str, value: Any, ttl: int | None = None) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
"""Set value in cache with TTL (async)."""
|
| 211 |
client = self._get_async_client()
|
| 212 |
if not client:
|
|
|
|
| 252 |
logger.warning(f"Async cache exists error for {key}: {e}")
|
| 253 |
return False
|
| 254 |
|
| 255 |
+
def get_summary_key(
|
| 256 |
+
self,
|
| 257 |
+
user_id: str,
|
| 258 |
+
start_date: str,
|
| 259 |
+
end_date: str,
|
| 260 |
+
project_id: str | None = None,
|
| 261 |
+
) -> str:
|
| 262 |
"""Generate cache key for summary stats."""
|
| 263 |
parts = ["summary", user_id, start_date, end_date]
|
| 264 |
if project_id:
|
| 265 |
parts.append(project_id)
|
| 266 |
return ":".join(parts)
|
| 267 |
|
| 268 |
+
def get_trends_key(
|
| 269 |
+
self, user_id: str, period: str, project_id: str | None = None
|
| 270 |
+
) -> str:
|
| 271 |
"""Generate cache key for trends data."""
|
| 272 |
parts = ["trends", user_id, period]
|
| 273 |
if project_id:
|
|
|
|
| 288 |
start_date: str,
|
| 289 |
end_date: str,
|
| 290 |
data: dict[str, Any],
|
| 291 |
+
project_id: str | None = None,
|
| 292 |
) -> bool:
|
| 293 |
"""Cache summary statistics."""
|
| 294 |
key = self.get_summary_key(user_id, start_date, end_date, project_id)
|
|
|
|
| 299 |
user_id: str,
|
| 300 |
start_date: str,
|
| 301 |
end_date: str,
|
| 302 |
+
project_id: str | None = None,
|
| 303 |
) -> dict[str, Any] | None:
|
| 304 |
"""Get cached summary statistics."""
|
| 305 |
key = self.get_summary_key(user_id, start_date, end_date, project_id)
|
|
|
|
| 310 |
user_id: str,
|
| 311 |
period: str,
|
| 312 |
data: dict[str, Any],
|
| 313 |
+
project_id: str | None = None,
|
| 314 |
) -> bool:
|
| 315 |
"""Cache trends data."""
|
| 316 |
key = self.get_trends_key(user_id, period, project_id)
|
| 317 |
return await self.aset(key, data, ttl=self.config.trends_ttl)
|
| 318 |
|
| 319 |
async def get_cached_trends(
|
| 320 |
+
self, user_id: str, period: str, project_id: str | None = None
|
|
|
|
|
|
|
|
|
|
| 321 |
) -> dict[str, Any] | None:
|
| 322 |
"""Get cached trends data."""
|
| 323 |
key = self.get_trends_key(user_id, period, project_id)
|
|
|
|
| 343 |
|
| 344 |
|
| 345 |
def cached(
|
| 346 |
+
key_func: Callable[..., str | None],
|
| 347 |
ttl: int | None = None,
|
| 348 |
+
cache_instance: UpstashRedisCache | None = None,
|
| 349 |
):
|
| 350 |
"""
|
| 351 |
+
Decorator for caching async function results with read-through semantics.
|
| 352 |
+
|
| 353 |
+
Flow: generate key → check cache → return cached if hit →
|
| 354 |
+
else call function → write result to cache → return result.
|
| 355 |
|
| 356 |
Args:
|
| 357 |
key_func: Function that generates cache key from function arguments
|
|
|
|
| 367 |
# expensive operation
|
| 368 |
return await fetch_from_langsmith(...)
|
| 369 |
"""
|
| 370 |
+
|
| 371 |
def decorator(func):
|
| 372 |
@wraps(func)
|
| 373 |
async def wrapper(*args, **kwargs):
|
| 374 |
cache = cache_instance or get_cache()
|
| 375 |
|
| 376 |
+
# 1. Generate cache key
|
| 377 |
+
try:
|
| 378 |
+
cache_key = key_func(*args, **kwargs)
|
| 379 |
+
except Exception as e:
|
| 380 |
+
logger.warning(f"Cache key generation failed, skipping cache: {e}")
|
| 381 |
+
return await func(*args, **kwargs)
|
| 382 |
+
|
| 383 |
+
# 2. Check cache (read-through)
|
| 384 |
+
if cache_key:
|
| 385 |
+
try:
|
| 386 |
+
cached_value = await cache.aget(cache_key)
|
| 387 |
+
if cached_value is not None:
|
| 388 |
+
logger.debug(f"Cache HIT for key: {cache_key}")
|
| 389 |
+
return cached_value
|
| 390 |
+
except Exception as e:
|
| 391 |
+
logger.warning(f"Cache read failed for {cache_key}: {e}")
|
| 392 |
+
|
| 393 |
+
# 3. Cache miss — call the wrapped function
|
| 394 |
result = await func(*args, **kwargs)
|
| 395 |
+
|
| 396 |
+
# 4. Write result to cache (skip None to avoid caching errors)
|
| 397 |
if result is not None and cache_key:
|
| 398 |
+
try:
|
| 399 |
+
await cache.aset(cache_key, result, ttl=ttl)
|
| 400 |
+
logger.debug(f"Cache SET for key: {cache_key}")
|
| 401 |
+
except Exception as e:
|
| 402 |
+
logger.warning(f"Cache write failed for {cache_key}: {e}")
|
| 403 |
+
|
| 404 |
return result
|
| 405 |
+
|
| 406 |
return wrapper
|
| 407 |
+
|
| 408 |
return decorator
|
| 409 |
|
| 410 |
|
|
|
|
| 438 |
"configured": cache.is_configured,
|
| 439 |
"status": "unknown",
|
| 440 |
"latency_ms": None,
|
| 441 |
+
"error": None,
|
| 442 |
}
|
| 443 |
|
| 444 |
if not cache.is_configured:
|
|
|
|
| 448 |
|
| 449 |
try:
|
| 450 |
import time
|
| 451 |
+
|
| 452 |
start = time.time()
|
| 453 |
|
| 454 |
# Ping test with a simple set/get
|
app/core/cost_control.py
CHANGED
|
@@ -10,12 +10,15 @@ Includes:
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import asyncio
|
|
|
|
| 13 |
import os
|
|
|
|
| 14 |
import time
|
| 15 |
from collections import defaultdict
|
| 16 |
from dataclasses import dataclass, field
|
| 17 |
from datetime import UTC, datetime
|
| 18 |
from enum import Enum
|
|
|
|
| 19 |
from typing import Any
|
| 20 |
|
| 21 |
from dotenv import load_dotenv
|
|
@@ -29,6 +32,7 @@ logger = get_logger("cost_control")
|
|
| 29 |
|
| 30 |
class CostAlertLevel(Enum):
|
| 31 |
"""Alert levels for cost monitoring."""
|
|
|
|
| 32 |
INFO = "info" # Normal usage
|
| 33 |
WARNING = "warning" # 70% of budget
|
| 34 |
CRITICAL = "critical" # 90% of budget
|
|
@@ -38,6 +42,7 @@ class CostAlertLevel(Enum):
|
|
| 38 |
@dataclass
|
| 39 |
class TokenUsage:
|
| 40 |
"""Token usage for a single request."""
|
|
|
|
| 41 |
prompt_tokens: int = 0
|
| 42 |
completion_tokens: int = 0
|
| 43 |
total_tokens: int = 0
|
|
@@ -56,39 +61,125 @@ class TokenUsage:
|
|
| 56 |
@dataclass
|
| 57 |
class CostConfig:
|
| 58 |
"""Cost control configuration."""
|
|
|
|
| 59 |
max_tokens_per_request: int = int(os.getenv("MAX_TOKENS_PER_REQUEST", "50000"))
|
| 60 |
monthly_token_budget: int = int(os.getenv("MONTHLY_TOKEN_BUDGET", "10000000"))
|
| 61 |
warning_threshold: float = 0.7 # 70% of budget
|
| 62 |
critical_threshold: float = 0.9 # 90% of budget
|
| 63 |
|
| 64 |
# Cost per 1K tokens for different models (estimates)
|
| 65 |
-
model_costs: dict[str, float] = field(
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
| 71 |
|
| 72 |
|
| 73 |
class TokenTracker:
|
| 74 |
"""
|
| 75 |
Tracks token usage across requests for cost monitoring.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
"""
|
| 77 |
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
self.config = config or CostConfig()
|
| 80 |
self._usage_history: list[TokenUsage] = []
|
| 81 |
self._monthly_totals: dict[str, int] = defaultdict(int) # {month: total_tokens}
|
| 82 |
-
self._operation_totals: dict[str, int] = defaultdict(
|
|
|
|
|
|
|
| 83 |
self._max_history = 100000
|
| 84 |
self._lock = asyncio.Lock()
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
async def record_usage(
|
| 87 |
self,
|
| 88 |
prompt_tokens: int,
|
| 89 |
completion_tokens: int,
|
| 90 |
model: str,
|
| 91 |
-
operation: str = "unknown"
|
| 92 |
) -> TokenUsage:
|
| 93 |
"""Record token usage for a request."""
|
| 94 |
total = prompt_tokens + completion_tokens
|
|
@@ -100,7 +191,7 @@ class TokenTracker:
|
|
| 100 |
total_tokens=total,
|
| 101 |
model=model,
|
| 102 |
operation=operation,
|
| 103 |
-
cost_estimate=cost
|
| 104 |
)
|
| 105 |
|
| 106 |
async with self._lock:
|
|
@@ -115,19 +206,22 @@ class TokenTracker:
|
|
| 115 |
|
| 116 |
# Trim history if needed
|
| 117 |
if len(self._usage_history) > self._max_history:
|
| 118 |
-
self._usage_history = self._usage_history[-self._max_history:]
|
| 119 |
|
| 120 |
# Check budget alerts
|
| 121 |
self._check_budget_alerts()
|
| 122 |
|
|
|
|
|
|
|
|
|
|
| 123 |
logger.info(
|
| 124 |
"Token usage recorded",
|
| 125 |
data={
|
| 126 |
"operation": operation,
|
| 127 |
"model": model,
|
| 128 |
"tokens": total,
|
| 129 |
-
"cost_estimate": cost
|
| 130 |
-
}
|
| 131 |
)
|
| 132 |
|
| 133 |
return usage
|
|
@@ -135,8 +229,7 @@ class TokenTracker:
|
|
| 135 |
def _estimate_cost(self, tokens: int, model: str) -> float:
|
| 136 |
"""Estimate cost based on tokens and model."""
|
| 137 |
cost_per_1k = self.config.model_costs.get(
|
| 138 |
-
model,
|
| 139 |
-
self.config.model_costs["default"]
|
| 140 |
)
|
| 141 |
return (tokens / 1000) * cost_per_1k
|
| 142 |
|
|
@@ -163,8 +256,8 @@ class TokenTracker:
|
|
| 163 |
"level": level.value,
|
| 164 |
"current_usage": current_usage,
|
| 165 |
"budget": budget,
|
| 166 |
-
"usage_percent": round(ratio * 100, 2)
|
| 167 |
-
}
|
| 168 |
)
|
| 169 |
|
| 170 |
def check_request_budget(self, estimated_tokens: int) -> bool:
|
|
@@ -178,8 +271,8 @@ class TokenTracker:
|
|
| 178 |
"Request exceeds per-request token limit",
|
| 179 |
data={
|
| 180 |
"estimated_tokens": estimated_tokens,
|
| 181 |
-
"limit": self.config.max_tokens_per_request
|
| 182 |
-
}
|
| 183 |
)
|
| 184 |
return False
|
| 185 |
|
|
@@ -193,8 +286,8 @@ class TokenTracker:
|
|
| 193 |
data={
|
| 194 |
"current_usage": current_usage,
|
| 195 |
"estimated_tokens": estimated_tokens,
|
| 196 |
-
"budget": self.config.monthly_token_budget
|
| 197 |
-
}
|
| 198 |
)
|
| 199 |
return False
|
| 200 |
|
|
@@ -214,7 +307,9 @@ class TokenTracker:
|
|
| 214 |
"monthly_tokens_used": current_monthly,
|
| 215 |
"monthly_budget": budget,
|
| 216 |
"budget_remaining": max(0, budget - current_monthly),
|
| 217 |
-
"budget_used_percent": round((current_monthly / budget) * 100, 2)
|
|
|
|
|
|
|
| 218 |
"total_cost_estimate": round(total_cost, 4),
|
| 219 |
"tokens_by_operation": dict(self._operation_totals),
|
| 220 |
"total_requests": len(self._usage_history),
|
|
@@ -250,6 +345,7 @@ def get_token_tracker() -> TokenTracker:
|
|
| 250 |
@dataclass
|
| 251 |
class LLMProvider:
|
| 252 |
"""Configuration for an LLM provider."""
|
|
|
|
| 253 |
name: str
|
| 254 |
model: str
|
| 255 |
base_url: str
|
|
@@ -276,13 +372,17 @@ class LLMProviderManager:
|
|
| 276 |
def _setup_default_providers(self):
|
| 277 |
"""Setup default provider configuration."""
|
| 278 |
# Primary provider - NVIDIA
|
| 279 |
-
self.register_provider(
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
# Add backup providers here if available
|
| 288 |
# self.register_provider(LLMProvider(
|
|
@@ -311,11 +411,14 @@ class LLMProviderManager:
|
|
| 311 |
):
|
| 312 |
provider.is_healthy = True
|
| 313 |
provider.failure_count = 0
|
| 314 |
-
logger.info(
|
|
|
|
|
|
|
| 315 |
|
| 316 |
# Get healthy providers sorted by priority
|
| 317 |
healthy = [
|
| 318 |
-
p
|
|
|
|
| 319 |
if p.is_healthy and os.getenv(p.api_key_env)
|
| 320 |
]
|
| 321 |
|
|
@@ -338,7 +441,7 @@ class LLMProviderManager:
|
|
| 338 |
provider.is_healthy = False
|
| 339 |
logger.error(
|
| 340 |
f"Provider {provider_name} marked unhealthy after {provider.failure_count} failures",
|
| 341 |
-
data={"error": str(error)}
|
| 342 |
)
|
| 343 |
|
| 344 |
async def mark_success(self, provider_name: str):
|
|
@@ -396,8 +499,8 @@ class BatchProcessor:
|
|
| 396 |
|
| 397 |
# Check if we should flush
|
| 398 |
should_flush = (
|
| 399 |
-
len(self._pending) >= self.batch_size
|
| 400 |
-
time.time() - self._last_flush >= self.batch_timeout
|
| 401 |
)
|
| 402 |
|
| 403 |
if should_flush:
|
|
@@ -431,6 +534,7 @@ class BatchProcessor:
|
|
| 431 |
|
| 432 |
class BudgetExceededError(Exception):
|
| 433 |
"""Raised when a request would exceed the budget."""
|
|
|
|
| 434 |
pass
|
| 435 |
|
| 436 |
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import asyncio
|
| 13 |
+
import json
|
| 14 |
import os
|
| 15 |
+
import tempfile
|
| 16 |
import time
|
| 17 |
from collections import defaultdict
|
| 18 |
from dataclasses import dataclass, field
|
| 19 |
from datetime import UTC, datetime
|
| 20 |
from enum import Enum
|
| 21 |
+
from pathlib import Path
|
| 22 |
from typing import Any
|
| 23 |
|
| 24 |
from dotenv import load_dotenv
|
|
|
|
| 32 |
|
| 33 |
class CostAlertLevel(Enum):
|
| 34 |
"""Alert levels for cost monitoring."""
|
| 35 |
+
|
| 36 |
INFO = "info" # Normal usage
|
| 37 |
WARNING = "warning" # 70% of budget
|
| 38 |
CRITICAL = "critical" # 90% of budget
|
|
|
|
| 42 |
@dataclass
|
| 43 |
class TokenUsage:
|
| 44 |
"""Token usage for a single request."""
|
| 45 |
+
|
| 46 |
prompt_tokens: int = 0
|
| 47 |
completion_tokens: int = 0
|
| 48 |
total_tokens: int = 0
|
|
|
|
| 61 |
@dataclass
|
| 62 |
class CostConfig:
|
| 63 |
"""Cost control configuration."""
|
| 64 |
+
|
| 65 |
max_tokens_per_request: int = int(os.getenv("MAX_TOKENS_PER_REQUEST", "50000"))
|
| 66 |
monthly_token_budget: int = int(os.getenv("MONTHLY_TOKEN_BUDGET", "10000000"))
|
| 67 |
warning_threshold: float = 0.7 # 70% of budget
|
| 68 |
critical_threshold: float = 0.9 # 90% of budget
|
| 69 |
|
| 70 |
# Cost per 1K tokens for different models (estimates)
|
| 71 |
+
model_costs: dict[str, float] = field(
|
| 72 |
+
default_factory=lambda: {
|
| 73 |
+
"meta/llama-3.1-70b-instruct": 0.0027,
|
| 74 |
+
"google/gemma-3-1b-it": 0.0005,
|
| 75 |
+
"nvidia/nv-embed-v1": 0.0001,
|
| 76 |
+
"default": 0.002,
|
| 77 |
+
}
|
| 78 |
+
)
|
| 79 |
|
| 80 |
|
| 81 |
class TokenTracker:
|
| 82 |
"""
|
| 83 |
Tracks token usage across requests for cost monitoring.
|
| 84 |
+
|
| 85 |
+
Monthly and operation totals are persisted to a JSON file so that
|
| 86 |
+
budget enforcement survives process restarts. Writes are atomic
|
| 87 |
+
(temp-file + ``os.replace``) to avoid corruption.
|
| 88 |
"""
|
| 89 |
|
| 90 |
+
_DEFAULT_PERSISTENCE_DIR = os.getenv(
|
| 91 |
+
"TOKEN_USAGE_DIR",
|
| 92 |
+
os.path.join(os.path.dirname(__file__), "..", "..", "data"),
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
def __init__(
|
| 96 |
+
self,
|
| 97 |
+
config: CostConfig | None = None,
|
| 98 |
+
persistence_path: str | Path | None = None,
|
| 99 |
+
):
|
| 100 |
self.config = config or CostConfig()
|
| 101 |
self._usage_history: list[TokenUsage] = []
|
| 102 |
self._monthly_totals: dict[str, int] = defaultdict(int) # {month: total_tokens}
|
| 103 |
+
self._operation_totals: dict[str, int] = defaultdict(
|
| 104 |
+
int
|
| 105 |
+
) # {operation: total_tokens}
|
| 106 |
self._max_history = 100000
|
| 107 |
self._lock = asyncio.Lock()
|
| 108 |
|
| 109 |
+
# --- Persistence setup ---
|
| 110 |
+
if persistence_path is not None:
|
| 111 |
+
self._persistence_path: Path | None = Path(persistence_path)
|
| 112 |
+
else:
|
| 113 |
+
self._persistence_path = (
|
| 114 |
+
Path(self._DEFAULT_PERSISTENCE_DIR) / "token_usage.json"
|
| 115 |
+
)
|
| 116 |
+
self._load_persisted_state()
|
| 117 |
+
|
| 118 |
+
# ------------------------------------------------------------------
|
| 119 |
+
# Persistence helpers
|
| 120 |
+
# ------------------------------------------------------------------
|
| 121 |
+
|
| 122 |
+
def _load_persisted_state(self) -> None:
|
| 123 |
+
"""Load monthly/operation totals from the persistence file (if it exists)."""
|
| 124 |
+
if self._persistence_path is None:
|
| 125 |
+
return
|
| 126 |
+
try:
|
| 127 |
+
if self._persistence_path.exists():
|
| 128 |
+
raw = self._persistence_path.read_text(encoding="utf-8")
|
| 129 |
+
data = json.loads(raw)
|
| 130 |
+
for k, v in data.get("monthly_totals", {}).items():
|
| 131 |
+
self._monthly_totals[k] = int(v)
|
| 132 |
+
for k, v in data.get("operation_totals", {}).items():
|
| 133 |
+
self._operation_totals[k] = int(v)
|
| 134 |
+
logger.info(
|
| 135 |
+
"Loaded persisted token usage",
|
| 136 |
+
data={"path": str(self._persistence_path)},
|
| 137 |
+
)
|
| 138 |
+
except Exception as exc:
|
| 139 |
+
# Never crash the app because of a corrupt state file
|
| 140 |
+
logger.warning(
|
| 141 |
+
f"Failed to load persisted token state, starting fresh: {exc}"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
def _persist_state(self) -> None:
|
| 145 |
+
"""Atomically write monthly/operation totals to disk.
|
| 146 |
+
|
| 147 |
+
Uses a temporary file in the same directory followed by
|
| 148 |
+
``os.replace`` to guarantee an atomic swap on POSIX systems.
|
| 149 |
+
"""
|
| 150 |
+
if self._persistence_path is None:
|
| 151 |
+
return
|
| 152 |
+
try:
|
| 153 |
+
self._persistence_path.parent.mkdir(parents=True, exist_ok=True)
|
| 154 |
+
payload = json.dumps(
|
| 155 |
+
{
|
| 156 |
+
"monthly_totals": dict(self._monthly_totals),
|
| 157 |
+
"operation_totals": dict(self._operation_totals),
|
| 158 |
+
"last_updated": datetime.now(UTC).isoformat(),
|
| 159 |
+
},
|
| 160 |
+
indent=2,
|
| 161 |
+
)
|
| 162 |
+
# Atomic write: write to temp file, then rename
|
| 163 |
+
fd, tmp_path = tempfile.mkstemp(
|
| 164 |
+
dir=str(self._persistence_path.parent),
|
| 165 |
+
prefix=".token_usage_",
|
| 166 |
+
suffix=".tmp",
|
| 167 |
+
)
|
| 168 |
+
try:
|
| 169 |
+
os.write(fd, payload.encode("utf-8"))
|
| 170 |
+
os.fsync(fd)
|
| 171 |
+
finally:
|
| 172 |
+
os.close(fd)
|
| 173 |
+
os.replace(tmp_path, str(self._persistence_path))
|
| 174 |
+
except Exception as exc:
|
| 175 |
+
logger.warning(f"Failed to persist token state: {exc}")
|
| 176 |
+
|
| 177 |
async def record_usage(
|
| 178 |
self,
|
| 179 |
prompt_tokens: int,
|
| 180 |
completion_tokens: int,
|
| 181 |
model: str,
|
| 182 |
+
operation: str = "unknown",
|
| 183 |
) -> TokenUsage:
|
| 184 |
"""Record token usage for a request."""
|
| 185 |
total = prompt_tokens + completion_tokens
|
|
|
|
| 191 |
total_tokens=total,
|
| 192 |
model=model,
|
| 193 |
operation=operation,
|
| 194 |
+
cost_estimate=cost,
|
| 195 |
)
|
| 196 |
|
| 197 |
async with self._lock:
|
|
|
|
| 206 |
|
| 207 |
# Trim history if needed
|
| 208 |
if len(self._usage_history) > self._max_history:
|
| 209 |
+
self._usage_history = self._usage_history[-self._max_history :]
|
| 210 |
|
| 211 |
# Check budget alerts
|
| 212 |
self._check_budget_alerts()
|
| 213 |
|
| 214 |
+
# Persist updated totals to disk (atomic write)
|
| 215 |
+
self._persist_state()
|
| 216 |
+
|
| 217 |
logger.info(
|
| 218 |
"Token usage recorded",
|
| 219 |
data={
|
| 220 |
"operation": operation,
|
| 221 |
"model": model,
|
| 222 |
"tokens": total,
|
| 223 |
+
"cost_estimate": cost,
|
| 224 |
+
},
|
| 225 |
)
|
| 226 |
|
| 227 |
return usage
|
|
|
|
| 229 |
def _estimate_cost(self, tokens: int, model: str) -> float:
|
| 230 |
"""Estimate cost based on tokens and model."""
|
| 231 |
cost_per_1k = self.config.model_costs.get(
|
| 232 |
+
model, self.config.model_costs["default"]
|
|
|
|
| 233 |
)
|
| 234 |
return (tokens / 1000) * cost_per_1k
|
| 235 |
|
|
|
|
| 256 |
"level": level.value,
|
| 257 |
"current_usage": current_usage,
|
| 258 |
"budget": budget,
|
| 259 |
+
"usage_percent": round(ratio * 100, 2),
|
| 260 |
+
},
|
| 261 |
)
|
| 262 |
|
| 263 |
def check_request_budget(self, estimated_tokens: int) -> bool:
|
|
|
|
| 271 |
"Request exceeds per-request token limit",
|
| 272 |
data={
|
| 273 |
"estimated_tokens": estimated_tokens,
|
| 274 |
+
"limit": self.config.max_tokens_per_request,
|
| 275 |
+
},
|
| 276 |
)
|
| 277 |
return False
|
| 278 |
|
|
|
|
| 286 |
data={
|
| 287 |
"current_usage": current_usage,
|
| 288 |
"estimated_tokens": estimated_tokens,
|
| 289 |
+
"budget": self.config.monthly_token_budget,
|
| 290 |
+
},
|
| 291 |
)
|
| 292 |
return False
|
| 293 |
|
|
|
|
| 307 |
"monthly_tokens_used": current_monthly,
|
| 308 |
"monthly_budget": budget,
|
| 309 |
"budget_remaining": max(0, budget - current_monthly),
|
| 310 |
+
"budget_used_percent": round((current_monthly / budget) * 100, 2)
|
| 311 |
+
if budget > 0
|
| 312 |
+
else 0,
|
| 313 |
"total_cost_estimate": round(total_cost, 4),
|
| 314 |
"tokens_by_operation": dict(self._operation_totals),
|
| 315 |
"total_requests": len(self._usage_history),
|
|
|
|
| 345 |
@dataclass
|
| 346 |
class LLMProvider:
|
| 347 |
"""Configuration for an LLM provider."""
|
| 348 |
+
|
| 349 |
name: str
|
| 350 |
model: str
|
| 351 |
base_url: str
|
|
|
|
| 372 |
def _setup_default_providers(self):
|
| 373 |
"""Setup default provider configuration."""
|
| 374 |
# Primary provider - NVIDIA
|
| 375 |
+
self.register_provider(
|
| 376 |
+
LLMProvider(
|
| 377 |
+
name="nvidia",
|
| 378 |
+
model=os.getenv("CHAT_MODEL", "meta/llama-3.1-70b-instruct"),
|
| 379 |
+
base_url=os.getenv(
|
| 380 |
+
"NVIDIA_BASE_URL", "https://integrate.api.nvidia.com/v1"
|
| 381 |
+
),
|
| 382 |
+
api_key_env="NVIDIA_API_KEY",
|
| 383 |
+
priority=0,
|
| 384 |
+
)
|
| 385 |
+
)
|
| 386 |
|
| 387 |
# Add backup providers here if available
|
| 388 |
# self.register_provider(LLMProvider(
|
|
|
|
| 411 |
):
|
| 412 |
provider.is_healthy = True
|
| 413 |
provider.failure_count = 0
|
| 414 |
+
logger.info(
|
| 415 |
+
f"Provider {provider.name} marked as healthy (recovery)"
|
| 416 |
+
)
|
| 417 |
|
| 418 |
# Get healthy providers sorted by priority
|
| 419 |
healthy = [
|
| 420 |
+
p
|
| 421 |
+
for p in self._providers.values()
|
| 422 |
if p.is_healthy and os.getenv(p.api_key_env)
|
| 423 |
]
|
| 424 |
|
|
|
|
| 441 |
provider.is_healthy = False
|
| 442 |
logger.error(
|
| 443 |
f"Provider {provider_name} marked unhealthy after {provider.failure_count} failures",
|
| 444 |
+
data={"error": str(error)},
|
| 445 |
)
|
| 446 |
|
| 447 |
async def mark_success(self, provider_name: str):
|
|
|
|
| 499 |
|
| 500 |
# Check if we should flush
|
| 501 |
should_flush = (
|
| 502 |
+
len(self._pending) >= self.batch_size
|
| 503 |
+
or time.time() - self._last_flush >= self.batch_timeout
|
| 504 |
)
|
| 505 |
|
| 506 |
if should_flush:
|
|
|
|
| 534 |
|
| 535 |
class BudgetExceededError(Exception):
|
| 536 |
"""Raised when a request would exceed the budget."""
|
| 537 |
+
|
| 538 |
pass
|
| 539 |
|
| 540 |
|
app/core/database.py
CHANGED
|
@@ -2,16 +2,17 @@ import logging
|
|
| 2 |
import os
|
| 3 |
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
-
from sqlalchemy import create_engine
|
| 6 |
-
from sqlalchemy.ext.declarative import declarative_base
|
| 7 |
from sqlalchemy.orm import sessionmaker
|
| 8 |
from sqlalchemy.pool import QueuePool
|
| 9 |
|
|
|
|
|
|
|
| 10 |
load_dotenv()
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
| 14 |
-
DATABASE_URL = os.getenv("DATABASE_URL")
|
| 15 |
|
| 16 |
if not DATABASE_URL:
|
| 17 |
# Fallback for local dev if not set
|
|
@@ -19,18 +20,20 @@ if not DATABASE_URL:
|
|
| 19 |
DATABASE_URL = "sqlite:///./test.db"
|
| 20 |
|
| 21 |
# Configure engine with connection pooling and SSL for Neon
|
| 22 |
-
engine_args = {
|
| 23 |
"pool_pre_ping": True, # Verify connections before using
|
| 24 |
"pool_recycle": 300, # Recycle connections every 5 minutes
|
| 25 |
}
|
| 26 |
|
| 27 |
# Only add pooling for non-SQLite databases
|
| 28 |
if not DATABASE_URL.startswith("sqlite"):
|
| 29 |
-
engine_args.update(
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
# SSL is handled via the connection string for Neon (sslmode=require)
|
| 35 |
logger.info("Using PostgreSQL with connection pooling")
|
| 36 |
else:
|
|
@@ -41,8 +44,6 @@ else:
|
|
| 41 |
engine = create_engine(DATABASE_URL, **engine_args)
|
| 42 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 43 |
|
| 44 |
-
Base = declarative_base()
|
| 45 |
-
|
| 46 |
|
| 47 |
def get_db():
|
| 48 |
"""
|
|
@@ -62,10 +63,138 @@ def check_db_connection() -> bool:
|
|
| 62 |
Returns True if connection successful, False otherwise.
|
| 63 |
"""
|
| 64 |
try:
|
| 65 |
-
from sqlalchemy import text
|
| 66 |
with engine.connect() as conn:
|
| 67 |
conn.execute(text("SELECT 1"))
|
| 68 |
return True
|
| 69 |
except Exception as e:
|
| 70 |
logger.error(f"Database connection check failed: {e}")
|
| 71 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
+
from sqlalchemy import create_engine, text
|
|
|
|
| 6 |
from sqlalchemy.orm import sessionmaker
|
| 7 |
from sqlalchemy.pool import QueuePool
|
| 8 |
|
| 9 |
+
from .models import Base
|
| 10 |
+
|
| 11 |
load_dotenv()
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
| 15 |
+
DATABASE_URL = os.getenv("DATABASE_URL") or ""
|
| 16 |
|
| 17 |
if not DATABASE_URL:
|
| 18 |
# Fallback for local dev if not set
|
|
|
|
| 20 |
DATABASE_URL = "sqlite:///./test.db"
|
| 21 |
|
| 22 |
# Configure engine with connection pooling and SSL for Neon
|
| 23 |
+
engine_args: dict[str, object] = {
|
| 24 |
"pool_pre_ping": True, # Verify connections before using
|
| 25 |
"pool_recycle": 300, # Recycle connections every 5 minutes
|
| 26 |
}
|
| 27 |
|
| 28 |
# Only add pooling for non-SQLite databases
|
| 29 |
if not DATABASE_URL.startswith("sqlite"):
|
| 30 |
+
engine_args.update(
|
| 31 |
+
{
|
| 32 |
+
"poolclass": QueuePool,
|
| 33 |
+
"pool_size": 5,
|
| 34 |
+
"max_overflow": 10,
|
| 35 |
+
}
|
| 36 |
+
)
|
| 37 |
# SSL is handled via the connection string for Neon (sslmode=require)
|
| 38 |
logger.info("Using PostgreSQL with connection pooling")
|
| 39 |
else:
|
|
|
|
| 44 |
engine = create_engine(DATABASE_URL, **engine_args)
|
| 45 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 46 |
|
|
|
|
|
|
|
| 47 |
|
| 48 |
def get_db():
|
| 49 |
"""
|
|
|
|
| 63 |
Returns True if connection successful, False otherwise.
|
| 64 |
"""
|
| 65 |
try:
|
|
|
|
| 66 |
with engine.connect() as conn:
|
| 67 |
conn.execute(text("SELECT 1"))
|
| 68 |
return True
|
| 69 |
except Exception as e:
|
| 70 |
logger.error(f"Database connection check failed: {e}")
|
| 71 |
return False
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _ensure_migrations_table():
|
| 75 |
+
"""Create migrations tracking table if it doesn't exist."""
|
| 76 |
+
with engine.connect() as conn:
|
| 77 |
+
conn.execute(
|
| 78 |
+
text("""
|
| 79 |
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
| 80 |
+
id SERIAL PRIMARY KEY,
|
| 81 |
+
migration_name VARCHAR(255) UNIQUE NOT NULL,
|
| 82 |
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 83 |
+
)
|
| 84 |
+
""")
|
| 85 |
+
)
|
| 86 |
+
conn.commit()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _is_migration_applied(name: str) -> bool:
|
| 90 |
+
"""Check if a migration has already been applied."""
|
| 91 |
+
with engine.connect() as conn:
|
| 92 |
+
result = conn.execute(
|
| 93 |
+
text("SELECT 1 FROM schema_migrations WHERE migration_name = :name"),
|
| 94 |
+
{"name": name},
|
| 95 |
+
)
|
| 96 |
+
return result.fetchone() is not None
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _record_migration(name: str):
|
| 100 |
+
"""Record that a migration has been applied."""
|
| 101 |
+
with engine.connect() as conn:
|
| 102 |
+
conn.execute(
|
| 103 |
+
text("INSERT INTO schema_migrations (migration_name) VALUES (:name)"),
|
| 104 |
+
{"name": name},
|
| 105 |
+
)
|
| 106 |
+
conn.commit()
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def run_migrations():
|
| 110 |
+
"""
|
| 111 |
+
Run database migrations to ensure schema is up to date.
|
| 112 |
+
|
| 113 |
+
Uses a migrations tracking table to ensure each migration
|
| 114 |
+
only runs once - safe for multiple server instances.
|
| 115 |
+
"""
|
| 116 |
+
_ensure_migrations_table()
|
| 117 |
+
|
| 118 |
+
if DATABASE_URL.startswith("sqlite"):
|
| 119 |
+
logger.info("SQLite database - using auto-create_all")
|
| 120 |
+
Base.metadata.create_all(bind=engine)
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
# PostgreSQL migrations
|
| 124 |
+
try:
|
| 125 |
+
with engine.connect() as conn:
|
| 126 |
+
# Ensure prd_documents table exists
|
| 127 |
+
result = conn.execute(
|
| 128 |
+
text("""
|
| 129 |
+
SELECT table_name
|
| 130 |
+
FROM information_schema.tables
|
| 131 |
+
WHERE table_name = 'prd_documents'
|
| 132 |
+
""")
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
if not result.fetchone():
|
| 136 |
+
logger.warning("prd_documents table not found - creating all tables")
|
| 137 |
+
Base.metadata.create_all(bind=engine)
|
| 138 |
+
_record_migration("create_all_tables")
|
| 139 |
+
return
|
| 140 |
+
|
| 141 |
+
# Define migrations - each with unique name
|
| 142 |
+
migrations = [
|
| 143 |
+
{
|
| 144 |
+
"name": "add_prd_product_vision",
|
| 145 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN product_vision TEXT",
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"name": "add_prd_key_features",
|
| 149 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN key_features JSONB",
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"name": "add_prd_user_stories",
|
| 153 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN user_stories JSONB",
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"name": "add_prd_assumptions",
|
| 157 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN assumptions TEXT",
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"name": "add_prd_judge_score",
|
| 161 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN judge_score INTEGER",
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"name": "add_prd_judge_approved",
|
| 165 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN judge_approved BOOLEAN DEFAULT FALSE",
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"name": "add_prd_judge_feedback",
|
| 169 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN judge_feedback TEXT",
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"name": "add_prd_phase",
|
| 173 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN phase VARCHAR(50) DEFAULT 'evaluating'",
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"name": "add_prd_follow_up_count",
|
| 177 |
+
"sql": "ALTER TABLE prd_documents ADD COLUMN follow_up_count INTEGER DEFAULT 0",
|
| 178 |
+
},
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
# Run each migration only if not already applied
|
| 182 |
+
for migration in migrations:
|
| 183 |
+
if not _is_migration_applied(migration["name"]):
|
| 184 |
+
try:
|
| 185 |
+
with engine.connect() as conn:
|
| 186 |
+
conn.execute(text(migration["sql"]))
|
| 187 |
+
conn.commit()
|
| 188 |
+
_record_migration(migration["name"])
|
| 189 |
+
logger.info(f"Applied migration: {migration['name']}")
|
| 190 |
+
except Exception as e:
|
| 191 |
+
# Column might already exist (race condition) - check and continue
|
| 192 |
+
if "duplicate" not in str(e).lower():
|
| 193 |
+
logger.warning(f"Migration {migration['name']} failed: {e}")
|
| 194 |
+
else:
|
| 195 |
+
logger.debug(f"Migration {migration['name']} already applied, skipping")
|
| 196 |
+
|
| 197 |
+
logger.info("Database migrations check completed")
|
| 198 |
+
|
| 199 |
+
except Exception as e:
|
| 200 |
+
logger.error(f"Migration check failed: {e}")
|
app/core/models.py
CHANGED
|
@@ -1,34 +1,101 @@
|
|
| 1 |
-
from
|
|
|
|
|
|
|
| 2 |
from sqlalchemy.dialects.postgresql import JSONB
|
| 3 |
-
from sqlalchemy.orm import relationship
|
| 4 |
from sqlalchemy.sql import func
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
class User(Base):
|
| 10 |
__tablename__ = "users"
|
| 11 |
|
| 12 |
-
id =
|
| 13 |
-
google_id
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
projects = relationship("Project", back_populates="owner")
|
| 20 |
|
| 21 |
class Project(Base):
|
| 22 |
__tablename__ = "projects"
|
| 23 |
|
| 24 |
-
id =
|
| 25 |
-
user_id =
|
| 26 |
-
title =
|
| 27 |
-
description =
|
| 28 |
# Using JSONB for Postgres, but if fallback to SQLite is needed, we might need a custom type or just Text.
|
| 29 |
# For now, we assume Postgres as requested.
|
| 30 |
-
artifacts =
|
| 31 |
-
created_at
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
|
| 3 |
+
from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, Integer, String, Text
|
| 4 |
from sqlalchemy.dialects.postgresql import JSONB
|
| 5 |
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
| 6 |
from sqlalchemy.sql import func
|
| 7 |
|
| 8 |
+
|
| 9 |
+
class Base(DeclarativeBase):
|
| 10 |
+
"""Base class for SQLAlchemy models."""
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
JSONType = JSON().with_variant(JSONB, "postgresql")
|
| 14 |
|
| 15 |
|
| 16 |
class User(Base):
|
| 17 |
__tablename__ = "users"
|
| 18 |
|
| 19 |
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
| 20 |
+
google_id: Mapped[str] = mapped_column(
|
| 21 |
+
String, unique=True, index=True, nullable=False
|
| 22 |
+
)
|
| 23 |
+
email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
|
| 24 |
+
full_name: Mapped[str | None] = mapped_column(String, nullable=True)
|
| 25 |
+
profile_picture: Mapped[str | None] = mapped_column(String, nullable=True)
|
| 26 |
+
created_at: Mapped[datetime] = mapped_column(
|
| 27 |
+
DateTime(timezone=True), server_default=func.now()
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner")
|
| 31 |
|
|
|
|
| 32 |
|
| 33 |
class Project(Base):
|
| 34 |
__tablename__ = "projects"
|
| 35 |
|
| 36 |
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
| 37 |
+
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
|
| 38 |
+
title: Mapped[str] = mapped_column(String, nullable=False)
|
| 39 |
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
| 40 |
# Using JSONB for Postgres, but if fallback to SQLite is needed, we might need a custom type or just Text.
|
| 41 |
# For now, we assume Postgres as requested.
|
| 42 |
+
artifacts: Mapped[dict[str, object]] = mapped_column(JSONType, nullable=False)
|
| 43 |
+
created_at: Mapped[datetime] = mapped_column(
|
| 44 |
+
DateTime(timezone=True), server_default=func.now()
|
| 45 |
+
)
|
| 46 |
+
updated_at: Mapped[datetime | None] = mapped_column(
|
| 47 |
+
DateTime(timezone=True), onupdate=func.now()
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
owner: Mapped["User"] = relationship("User", back_populates="projects")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class PRDDocument(Base):
|
| 54 |
+
__tablename__ = "prd_documents"
|
| 55 |
+
|
| 56 |
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
| 57 |
+
session_id: Mapped[str] = mapped_column(String, unique=True, index=True)
|
| 58 |
+
user_id: Mapped[int | None] = mapped_column(
|
| 59 |
+
Integer, ForeignKey("users.id"), nullable=True
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
initial_description: Mapped[str] = mapped_column(Text, nullable=False)
|
| 63 |
+
|
| 64 |
+
# JSONB to track which sections are satisfied
|
| 65 |
+
requirements_status: Mapped[dict[str, bool]] = mapped_column(
|
| 66 |
+
JSONType, nullable=False, default=dict
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# JSONB to store collected info tagged by section
|
| 70 |
+
collected_info: Mapped[dict[str, str]] = mapped_column(
|
| 71 |
+
JSONType, nullable=False, default=dict
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# Generated PRD content
|
| 75 |
+
generated_prd: Mapped[str | None] = mapped_column(Text, nullable=True)
|
| 76 |
+
|
| 77 |
+
# Structured PRD sections
|
| 78 |
+
product_vision: Mapped[str | None] = mapped_column(Text, nullable=True)
|
| 79 |
+
key_features: Mapped[dict[str, object] | None] = mapped_column(
|
| 80 |
+
JSONType, nullable=True
|
| 81 |
+
) # {"must_have": [], "should_have": [], "could_have": [], "wont_have": []}
|
| 82 |
+
user_stories: Mapped[list[dict[str, object]] | None] = mapped_column(
|
| 83 |
+
JSONType, nullable=True
|
| 84 |
+
) # [{"id": "US1", "as_a": "...", "i_want": "...", "so_that": "...", "acceptance_criteria": []}]
|
| 85 |
+
assumptions: Mapped[str | None] = mapped_column(Text, nullable=True)
|
| 86 |
+
|
| 87 |
+
# Judge result storage
|
| 88 |
+
judge_score: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
| 89 |
+
judge_approved: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
| 90 |
+
judge_feedback: Mapped[str | None] = mapped_column(Text, nullable=True)
|
| 91 |
+
|
| 92 |
+
# State tracking
|
| 93 |
+
phase: Mapped[str] = mapped_column(String, nullable=False, default="evaluating")
|
| 94 |
+
follow_up_count: Mapped[int] = mapped_column(Integer, default=0)
|
| 95 |
|
| 96 |
+
created_at: Mapped[datetime] = mapped_column(
|
| 97 |
+
DateTime(timezone=True), server_default=func.now()
|
| 98 |
+
)
|
| 99 |
+
updated_at: Mapped[datetime | None] = mapped_column(
|
| 100 |
+
DateTime(timezone=True), onupdate=func.now()
|
| 101 |
+
)
|
app/core/observability.py
CHANGED
|
@@ -6,6 +6,7 @@ Includes:
|
|
| 6 |
- Error tracking patterns
|
| 7 |
- Request/Response logging middleware
|
| 8 |
"""
|
|
|
|
| 9 |
import json
|
| 10 |
import logging
|
| 11 |
import os
|
|
@@ -39,6 +40,7 @@ class LogLevel(Enum):
|
|
| 39 |
@dataclass
|
| 40 |
class LogContext:
|
| 41 |
"""Structured log context."""
|
|
|
|
| 42 |
trace_id: str
|
| 43 |
span_id: str
|
| 44 |
timestamp: str
|
|
@@ -50,6 +52,7 @@ class LogContext:
|
|
| 50 |
@dataclass
|
| 51 |
class LogEntry:
|
| 52 |
"""Structured log entry."""
|
|
|
|
| 53 |
level: str
|
| 54 |
message: str
|
| 55 |
context: LogContext
|
|
@@ -82,14 +85,19 @@ class StructuredLogger:
|
|
| 82 |
|
| 83 |
# File handler only in development
|
| 84 |
if os.getenv("ENVIRONMENT") == "development":
|
| 85 |
-
log_dir = os.path.join(
|
|
|
|
|
|
|
| 86 |
os.makedirs(log_dir, exist_ok=True)
|
| 87 |
-
file_handler = logging.FileHandler(
|
|
|
|
|
|
|
| 88 |
file_handler.setFormatter(StructuredFormatter())
|
| 89 |
self.logger.addHandler(file_handler)
|
| 90 |
|
| 91 |
self.logger.setLevel(
|
| 92 |
-
logging.DEBUG
|
|
|
|
| 93 |
else logging.INFO
|
| 94 |
)
|
| 95 |
|
|
@@ -107,7 +115,7 @@ class StructuredLogger:
|
|
| 107 |
message: str,
|
| 108 |
data: dict[str, Any] | None = None,
|
| 109 |
error: Exception | None = None,
|
| 110 |
-
duration_ms: float | None = None
|
| 111 |
):
|
| 112 |
"""Internal logging method."""
|
| 113 |
entry = LogEntry(
|
|
@@ -126,18 +134,20 @@ class StructuredLogger:
|
|
| 126 |
# Remove None values
|
| 127 |
log_dict = {k: v for k, v in log_dict.items() if v is not None}
|
| 128 |
if "context" in log_dict:
|
| 129 |
-
log_dict["context"] = {
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
self.logger.log(
|
| 132 |
-
getattr(logging, level.value),
|
| 133 |
-
json.dumps(log_dict)
|
| 134 |
-
)
|
| 135 |
|
| 136 |
def _get_stack_trace(self, error: Exception | None) -> str | None:
|
| 137 |
"""Get stack trace from exception."""
|
| 138 |
if error:
|
| 139 |
import traceback
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
| 141 |
return None
|
| 142 |
|
| 143 |
def debug(self, message: str, data: dict[str, Any] | None = None):
|
|
@@ -146,13 +156,28 @@ class StructuredLogger:
|
|
| 146 |
def info(self, message: str, data: dict[str, Any] | None = None):
|
| 147 |
self._log(LogLevel.INFO, message, data)
|
| 148 |
|
| 149 |
-
def warning(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
self._log(LogLevel.WARNING, message, data, error)
|
| 151 |
|
| 152 |
-
def error(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
self._log(LogLevel.ERROR, message, data, error)
|
| 154 |
|
| 155 |
-
def critical(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
self._log(LogLevel.CRITICAL, message, data, error)
|
| 157 |
|
| 158 |
def log_request(
|
|
@@ -162,7 +187,7 @@ class StructuredLogger:
|
|
| 162 |
status_code: int,
|
| 163 |
duration_ms: float,
|
| 164 |
user_id: str | None = None,
|
| 165 |
-
extra: dict[str, Any] | None = None
|
| 166 |
):
|
| 167 |
"""Log HTTP request with timing."""
|
| 168 |
data = {
|
|
@@ -170,9 +195,14 @@ class StructuredLogger:
|
|
| 170 |
"http_path": path,
|
| 171 |
"http_status": status_code,
|
| 172 |
"user_id": user_id,
|
| 173 |
-
**(extra or {})
|
| 174 |
}
|
| 175 |
-
self._log(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
|
| 178 |
class StructuredFormatter(logging.Formatter):
|
|
@@ -185,24 +215,30 @@ class StructuredFormatter(logging.Formatter):
|
|
| 185 |
return record.getMessage()
|
| 186 |
except (json.JSONDecodeError, TypeError):
|
| 187 |
# Format as structured JSON for non-structured logs
|
| 188 |
-
return json.dumps(
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
-
#
|
| 197 |
-
|
| 198 |
|
| 199 |
|
| 200 |
def get_logger(name: str = "specs-before-code") -> StructuredLogger:
|
| 201 |
-
"""
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
|
| 208 |
def set_trace_id(trace_id: str):
|
|
@@ -230,6 +266,7 @@ def set_span_id(span_id: str):
|
|
| 230 |
@dataclass
|
| 231 |
class PerformanceMetrics:
|
| 232 |
"""Container for performance metrics."""
|
|
|
|
| 233 |
operation: str
|
| 234 |
duration_ms: float
|
| 235 |
success: bool
|
|
@@ -250,33 +287,42 @@ class PerformanceMonitor:
|
|
| 250 |
self._metrics: list[PerformanceMetrics] = []
|
| 251 |
self._max_metrics = 10000 # Keep last N metrics in memory
|
| 252 |
|
| 253 |
-
def record(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
"""Record a performance metric."""
|
| 255 |
metric = PerformanceMetrics(
|
| 256 |
operation=operation,
|
| 257 |
duration_ms=duration_ms,
|
| 258 |
success=success,
|
| 259 |
-
metadata=metadata or {}
|
| 260 |
)
|
| 261 |
self._metrics.append(metric)
|
| 262 |
|
| 263 |
# Trim old metrics
|
| 264 |
if len(self._metrics) > self._max_metrics:
|
| 265 |
-
self._metrics = self._metrics[-self._max_metrics:]
|
| 266 |
|
| 267 |
# Log slow operations
|
| 268 |
if duration_ms > 5000: # 5 seconds
|
| 269 |
get_logger().warning(
|
| 270 |
f"Slow operation detected: {operation}",
|
| 271 |
-
data={"duration_ms": duration_ms, **metric.metadata}
|
| 272 |
)
|
| 273 |
|
| 274 |
-
def get_stats(
|
|
|
|
|
|
|
| 275 |
"""Get performance statistics for an operation."""
|
| 276 |
cutoff = datetime.now(UTC).timestamp() - window_seconds
|
| 277 |
|
| 278 |
filtered = [
|
| 279 |
-
m
|
|
|
|
| 280 |
if (operation is None or m.operation == operation)
|
| 281 |
and datetime.fromisoformat(m.timestamp).timestamp() > cutoff
|
| 282 |
]
|
|
@@ -295,17 +341,18 @@ class PerformanceMonitor:
|
|
| 295 |
"min_duration_ms": min(durations),
|
| 296 |
"max_duration_ms": max(durations),
|
| 297 |
"p50_duration_ms": sorted(durations)[len(durations) // 2],
|
| 298 |
-
"p95_duration_ms": sorted(durations)[int(len(durations) * 0.95)]
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
}
|
| 301 |
|
| 302 |
def get_all_stats(self, window_seconds: int = 300) -> dict[str, dict[str, Any]]:
|
| 303 |
"""Get statistics for all operations."""
|
| 304 |
operations = {m.operation for m in self._metrics}
|
| 305 |
-
return {
|
| 306 |
-
op: self.get_stats(op, window_seconds)
|
| 307 |
-
for op in operations
|
| 308 |
-
}
|
| 309 |
|
| 310 |
|
| 311 |
# Global performance monitor
|
|
@@ -329,6 +376,7 @@ def timed(operation_name: str | None = None):
|
|
| 329 |
async def call_llm():
|
| 330 |
...
|
| 331 |
"""
|
|
|
|
| 332 |
def decorator(func: Callable) -> Callable:
|
| 333 |
name = operation_name or f"{func.__module__}.{func.__name__}"
|
| 334 |
|
|
@@ -347,7 +395,10 @@ def timed(operation_name: str | None = None):
|
|
| 347 |
operation=name,
|
| 348 |
duration_ms=duration_ms,
|
| 349 |
success=success,
|
| 350 |
-
metadata={
|
|
|
|
|
|
|
|
|
|
| 351 |
)
|
| 352 |
|
| 353 |
@wraps(func)
|
|
@@ -365,10 +416,14 @@ def timed(operation_name: str | None = None):
|
|
| 365 |
operation=name,
|
| 366 |
duration_ms=duration_ms,
|
| 367 |
success=success,
|
| 368 |
-
metadata={
|
|
|
|
|
|
|
|
|
|
| 369 |
)
|
| 370 |
|
| 371 |
import asyncio
|
|
|
|
| 372 |
if asyncio.iscoroutinefunction(func):
|
| 373 |
return async_wrapper
|
| 374 |
return sync_wrapper
|
|
@@ -389,7 +444,7 @@ class ErrorTracker:
|
|
| 389 |
self,
|
| 390 |
error: Exception,
|
| 391 |
context: dict[str, Any] | None = None,
|
| 392 |
-
severity: str = "error"
|
| 393 |
):
|
| 394 |
"""Track an error occurrence."""
|
| 395 |
error_entry = {
|
|
@@ -404,13 +459,11 @@ class ErrorTracker:
|
|
| 404 |
self._errors.append(error_entry)
|
| 405 |
|
| 406 |
if len(self._errors) > self._max_errors:
|
| 407 |
-
self._errors = self._errors[-self._max_errors:]
|
| 408 |
|
| 409 |
# Log the error
|
| 410 |
get_logger().error(
|
| 411 |
-
f"Error tracked: {type(error).__name__}",
|
| 412 |
-
data=error_entry,
|
| 413 |
-
error=error
|
| 414 |
)
|
| 415 |
|
| 416 |
def get_error_summary(self, window_seconds: int = 3600) -> dict[str, Any]:
|
|
@@ -418,7 +471,8 @@ class ErrorTracker:
|
|
| 418 |
cutoff = datetime.now(UTC).timestamp() - window_seconds
|
| 419 |
|
| 420 |
recent = [
|
| 421 |
-
e
|
|
|
|
| 422 |
if datetime.fromisoformat(e["timestamp"]).timestamp() > cutoff
|
| 423 |
]
|
| 424 |
|
|
@@ -446,6 +500,8 @@ def get_error_tracker() -> ErrorTracker:
|
|
| 446 |
return _error_tracker
|
| 447 |
|
| 448 |
|
| 449 |
-
def track_error(
|
|
|
|
|
|
|
| 450 |
"""Convenience function to track an error."""
|
| 451 |
get_error_tracker().track(error, context, severity)
|
|
|
|
| 6 |
- Error tracking patterns
|
| 7 |
- Request/Response logging middleware
|
| 8 |
"""
|
| 9 |
+
|
| 10 |
import json
|
| 11 |
import logging
|
| 12 |
import os
|
|
|
|
| 40 |
@dataclass
|
| 41 |
class LogContext:
|
| 42 |
"""Structured log context."""
|
| 43 |
+
|
| 44 |
trace_id: str
|
| 45 |
span_id: str
|
| 46 |
timestamp: str
|
|
|
|
| 52 |
@dataclass
|
| 53 |
class LogEntry:
|
| 54 |
"""Structured log entry."""
|
| 55 |
+
|
| 56 |
level: str
|
| 57 |
message: str
|
| 58 |
context: LogContext
|
|
|
|
| 85 |
|
| 86 |
# File handler only in development
|
| 87 |
if os.getenv("ENVIRONMENT") == "development":
|
| 88 |
+
log_dir = os.path.join(
|
| 89 |
+
os.path.dirname(os.path.abspath(__file__)), "../../logs"
|
| 90 |
+
)
|
| 91 |
os.makedirs(log_dir, exist_ok=True)
|
| 92 |
+
file_handler = logging.FileHandler(
|
| 93 |
+
os.path.join(log_dir, "app.log"), encoding="utf-8"
|
| 94 |
+
)
|
| 95 |
file_handler.setFormatter(StructuredFormatter())
|
| 96 |
self.logger.addHandler(file_handler)
|
| 97 |
|
| 98 |
self.logger.setLevel(
|
| 99 |
+
logging.DEBUG
|
| 100 |
+
if os.getenv("ENVIRONMENT") == "development"
|
| 101 |
else logging.INFO
|
| 102 |
)
|
| 103 |
|
|
|
|
| 115 |
message: str,
|
| 116 |
data: dict[str, Any] | None = None,
|
| 117 |
error: Exception | None = None,
|
| 118 |
+
duration_ms: float | None = None,
|
| 119 |
):
|
| 120 |
"""Internal logging method."""
|
| 121 |
entry = LogEntry(
|
|
|
|
| 134 |
# Remove None values
|
| 135 |
log_dict = {k: v for k, v in log_dict.items() if v is not None}
|
| 136 |
if "context" in log_dict:
|
| 137 |
+
log_dict["context"] = {
|
| 138 |
+
k: v for k, v in log_dict["context"].items() if v is not None
|
| 139 |
+
}
|
| 140 |
|
| 141 |
+
self.logger.log(getattr(logging, level.value), json.dumps(log_dict))
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
def _get_stack_trace(self, error: Exception | None) -> str | None:
|
| 144 |
"""Get stack trace from exception."""
|
| 145 |
if error:
|
| 146 |
import traceback
|
| 147 |
+
|
| 148 |
+
return "".join(
|
| 149 |
+
traceback.format_exception(type(error), error, error.__traceback__)
|
| 150 |
+
)
|
| 151 |
return None
|
| 152 |
|
| 153 |
def debug(self, message: str, data: dict[str, Any] | None = None):
|
|
|
|
| 156 |
def info(self, message: str, data: dict[str, Any] | None = None):
|
| 157 |
self._log(LogLevel.INFO, message, data)
|
| 158 |
|
| 159 |
+
def warning(
|
| 160 |
+
self,
|
| 161 |
+
message: str,
|
| 162 |
+
data: dict[str, Any] | None = None,
|
| 163 |
+
error: Exception | None = None,
|
| 164 |
+
):
|
| 165 |
self._log(LogLevel.WARNING, message, data, error)
|
| 166 |
|
| 167 |
+
def error(
|
| 168 |
+
self,
|
| 169 |
+
message: str,
|
| 170 |
+
data: dict[str, Any] | None = None,
|
| 171 |
+
error: Exception | None = None,
|
| 172 |
+
):
|
| 173 |
self._log(LogLevel.ERROR, message, data, error)
|
| 174 |
|
| 175 |
+
def critical(
|
| 176 |
+
self,
|
| 177 |
+
message: str,
|
| 178 |
+
data: dict[str, Any] | None = None,
|
| 179 |
+
error: Exception | None = None,
|
| 180 |
+
):
|
| 181 |
self._log(LogLevel.CRITICAL, message, data, error)
|
| 182 |
|
| 183 |
def log_request(
|
|
|
|
| 187 |
status_code: int,
|
| 188 |
duration_ms: float,
|
| 189 |
user_id: str | None = None,
|
| 190 |
+
extra: dict[str, Any] | None = None,
|
| 191 |
):
|
| 192 |
"""Log HTTP request with timing."""
|
| 193 |
data = {
|
|
|
|
| 195 |
"http_path": path,
|
| 196 |
"http_status": status_code,
|
| 197 |
"user_id": user_id,
|
| 198 |
+
**(extra or {}),
|
| 199 |
}
|
| 200 |
+
self._log(
|
| 201 |
+
LogLevel.INFO,
|
| 202 |
+
f"{method} {path} {status_code}",
|
| 203 |
+
data,
|
| 204 |
+
duration_ms=duration_ms,
|
| 205 |
+
)
|
| 206 |
|
| 207 |
|
| 208 |
class StructuredFormatter(logging.Formatter):
|
|
|
|
| 215 |
return record.getMessage()
|
| 216 |
except (json.JSONDecodeError, TypeError):
|
| 217 |
# Format as structured JSON for non-structured logs
|
| 218 |
+
return json.dumps(
|
| 219 |
+
{
|
| 220 |
+
"level": record.levelname,
|
| 221 |
+
"message": record.getMessage(),
|
| 222 |
+
"logger": record.name,
|
| 223 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
| 224 |
+
}
|
| 225 |
+
)
|
| 226 |
|
| 227 |
|
| 228 |
+
# Registry of named loggers (replaces broken singleton pattern)
|
| 229 |
+
_loggers: dict[str, StructuredLogger] = {}
|
| 230 |
|
| 231 |
|
| 232 |
def get_logger(name: str = "specs-before-code") -> StructuredLogger:
|
| 233 |
+
"""
|
| 234 |
+
Get or create a structured logger by name.
|
| 235 |
+
|
| 236 |
+
Uses a registry so each module gets its own stable logger instance.
|
| 237 |
+
Repeated calls with the same name return the same instance (no handler duplication).
|
| 238 |
+
"""
|
| 239 |
+
if name not in _loggers:
|
| 240 |
+
_loggers[name] = StructuredLogger(name)
|
| 241 |
+
return _loggers[name]
|
| 242 |
|
| 243 |
|
| 244 |
def set_trace_id(trace_id: str):
|
|
|
|
| 266 |
@dataclass
|
| 267 |
class PerformanceMetrics:
|
| 268 |
"""Container for performance metrics."""
|
| 269 |
+
|
| 270 |
operation: str
|
| 271 |
duration_ms: float
|
| 272 |
success: bool
|
|
|
|
| 287 |
self._metrics: list[PerformanceMetrics] = []
|
| 288 |
self._max_metrics = 10000 # Keep last N metrics in memory
|
| 289 |
|
| 290 |
+
def record(
|
| 291 |
+
self,
|
| 292 |
+
operation: str,
|
| 293 |
+
duration_ms: float,
|
| 294 |
+
success: bool,
|
| 295 |
+
metadata: dict[str, Any] | None = None,
|
| 296 |
+
):
|
| 297 |
"""Record a performance metric."""
|
| 298 |
metric = PerformanceMetrics(
|
| 299 |
operation=operation,
|
| 300 |
duration_ms=duration_ms,
|
| 301 |
success=success,
|
| 302 |
+
metadata=metadata or {},
|
| 303 |
)
|
| 304 |
self._metrics.append(metric)
|
| 305 |
|
| 306 |
# Trim old metrics
|
| 307 |
if len(self._metrics) > self._max_metrics:
|
| 308 |
+
self._metrics = self._metrics[-self._max_metrics :]
|
| 309 |
|
| 310 |
# Log slow operations
|
| 311 |
if duration_ms > 5000: # 5 seconds
|
| 312 |
get_logger().warning(
|
| 313 |
f"Slow operation detected: {operation}",
|
| 314 |
+
data={"duration_ms": duration_ms, **metric.metadata},
|
| 315 |
)
|
| 316 |
|
| 317 |
+
def get_stats(
|
| 318 |
+
self, operation: str | None = None, window_seconds: int = 300
|
| 319 |
+
) -> dict[str, Any]:
|
| 320 |
"""Get performance statistics for an operation."""
|
| 321 |
cutoff = datetime.now(UTC).timestamp() - window_seconds
|
| 322 |
|
| 323 |
filtered = [
|
| 324 |
+
m
|
| 325 |
+
for m in self._metrics
|
| 326 |
if (operation is None or m.operation == operation)
|
| 327 |
and datetime.fromisoformat(m.timestamp).timestamp() > cutoff
|
| 328 |
]
|
|
|
|
| 341 |
"min_duration_ms": min(durations),
|
| 342 |
"max_duration_ms": max(durations),
|
| 343 |
"p50_duration_ms": sorted(durations)[len(durations) // 2],
|
| 344 |
+
"p95_duration_ms": sorted(durations)[int(len(durations) * 0.95)]
|
| 345 |
+
if len(durations) > 1
|
| 346 |
+
else durations[0],
|
| 347 |
+
"p99_duration_ms": sorted(durations)[int(len(durations) * 0.99)]
|
| 348 |
+
if len(durations) > 1
|
| 349 |
+
else durations[0],
|
| 350 |
}
|
| 351 |
|
| 352 |
def get_all_stats(self, window_seconds: int = 300) -> dict[str, dict[str, Any]]:
|
| 353 |
"""Get statistics for all operations."""
|
| 354 |
operations = {m.operation for m in self._metrics}
|
| 355 |
+
return {op: self.get_stats(op, window_seconds) for op in operations}
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
|
| 358 |
# Global performance monitor
|
|
|
|
| 376 |
async def call_llm():
|
| 377 |
...
|
| 378 |
"""
|
| 379 |
+
|
| 380 |
def decorator(func: Callable) -> Callable:
|
| 381 |
name = operation_name or f"{func.__module__}.{func.__name__}"
|
| 382 |
|
|
|
|
| 395 |
operation=name,
|
| 396 |
duration_ms=duration_ms,
|
| 397 |
success=success,
|
| 398 |
+
metadata={
|
| 399 |
+
"args_count": len(args),
|
| 400 |
+
"kwargs_keys": list(kwargs.keys()),
|
| 401 |
+
},
|
| 402 |
)
|
| 403 |
|
| 404 |
@wraps(func)
|
|
|
|
| 416 |
operation=name,
|
| 417 |
duration_ms=duration_ms,
|
| 418 |
success=success,
|
| 419 |
+
metadata={
|
| 420 |
+
"args_count": len(args),
|
| 421 |
+
"kwargs_keys": list(kwargs.keys()),
|
| 422 |
+
},
|
| 423 |
)
|
| 424 |
|
| 425 |
import asyncio
|
| 426 |
+
|
| 427 |
if asyncio.iscoroutinefunction(func):
|
| 428 |
return async_wrapper
|
| 429 |
return sync_wrapper
|
|
|
|
| 444 |
self,
|
| 445 |
error: Exception,
|
| 446 |
context: dict[str, Any] | None = None,
|
| 447 |
+
severity: str = "error",
|
| 448 |
):
|
| 449 |
"""Track an error occurrence."""
|
| 450 |
error_entry = {
|
|
|
|
| 459 |
self._errors.append(error_entry)
|
| 460 |
|
| 461 |
if len(self._errors) > self._max_errors:
|
| 462 |
+
self._errors = self._errors[-self._max_errors :]
|
| 463 |
|
| 464 |
# Log the error
|
| 465 |
get_logger().error(
|
| 466 |
+
f"Error tracked: {type(error).__name__}", data=error_entry, error=error
|
|
|
|
|
|
|
| 467 |
)
|
| 468 |
|
| 469 |
def get_error_summary(self, window_seconds: int = 3600) -> dict[str, Any]:
|
|
|
|
| 471 |
cutoff = datetime.now(UTC).timestamp() - window_seconds
|
| 472 |
|
| 473 |
recent = [
|
| 474 |
+
e
|
| 475 |
+
for e in self._errors
|
| 476 |
if datetime.fromisoformat(e["timestamp"]).timestamp() > cutoff
|
| 477 |
]
|
| 478 |
|
|
|
|
| 500 |
return _error_tracker
|
| 501 |
|
| 502 |
|
| 503 |
+
def track_error(
|
| 504 |
+
error: Exception, context: dict[str, Any] | None = None, severity: str = "error"
|
| 505 |
+
):
|
| 506 |
"""Convenience function to track an error."""
|
| 507 |
get_error_tracker().track(error, context, severity)
|
app/core/orchestrator.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
|
|
| 1 |
from collections.abc import AsyncIterator
|
| 2 |
from typing import Annotated, Any, Literal, TypedDict
|
| 3 |
|
| 4 |
-
from langgraph.
|
|
|
|
| 5 |
|
| 6 |
from .agents import AgentSystem
|
| 7 |
from .rag import RAGService
|
| 8 |
from .schemas import AgentResponse, JudgeOutput, ProjectRequest, TeamRole
|
| 9 |
|
|
|
|
|
|
|
| 10 |
|
| 11 |
def merge_dicts(a: dict, b: dict) -> dict:
|
| 12 |
return {**a, **b}
|
|
@@ -30,10 +34,31 @@ class AgentState(TypedDict):
|
|
| 30 |
feedback: Annotated[str, replace_reducer]
|
| 31 |
retry_count: Annotated[int, replace_reducer]
|
| 32 |
judge_results: Annotated[dict[str, dict[str, Any]], merge_dicts]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
# Dependency mapping
|
| 36 |
-
|
|
|
|
| 37 |
TeamRole.PROJECT_REFINER: [],
|
| 38 |
TeamRole.PRODUCT_OWNER: [TeamRole.PROJECT_REFINER],
|
| 39 |
# Phase 2
|
|
@@ -53,7 +78,7 @@ AGENT_DEPENDENCIES = {
|
|
| 53 |
],
|
| 54 |
TeamRole.TECHNICAL_WRITER: [TeamRole.PRODUCT_OWNER], # Needs overview
|
| 55 |
# Phase 5
|
| 56 |
-
TeamRole.SPEC_COORDINATOR:
|
| 57 |
}
|
| 58 |
|
| 59 |
|
|
@@ -72,12 +97,25 @@ class Orchestrator:
|
|
| 72 |
self.max_retries = 2
|
| 73 |
self.graph = self._build_graph()
|
| 74 |
|
| 75 |
-
async def run_pipeline(
|
|
|
|
|
|
|
| 76 |
"""
|
| 77 |
Run the full multi-agent pipeline.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
"""
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
initial_state: AgentState = {
|
| 83 |
"context": initial_context,
|
|
@@ -88,17 +126,24 @@ class Orchestrator:
|
|
| 88 |
"feedback": "",
|
| 89 |
"retry_count": 0,
|
| 90 |
"judge_results": {},
|
|
|
|
| 91 |
}
|
| 92 |
|
| 93 |
final_state = await self.graph.ainvoke(initial_state)
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
return {
|
| 96 |
-
"
|
| 97 |
-
"
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
async def run_pipeline_streaming(
|
| 101 |
-
self, project_request: ProjectRequest
|
| 102 |
) -> AsyncIterator[dict[str, Any]]:
|
| 103 |
"""
|
| 104 |
Run the pipeline with streaming output for each agent.
|
|
@@ -114,9 +159,20 @@ class Orchestrator:
|
|
| 114 |
- pipeline_complete: Full pipeline finished with results
|
| 115 |
- error: Error occurred
|
| 116 |
"""
|
| 117 |
-
|
|
|
|
| 118 |
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
query = initial_context
|
| 122 |
docs = self.rag_service.retrieve(query, k=3)
|
|
@@ -131,21 +187,36 @@ class Orchestrator:
|
|
| 131 |
all_judge_results: dict[str, dict[str, Any]] = {}
|
| 132 |
history: list[AgentResponse] = []
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
for role in execution_order:
|
| 151 |
yield {"type": "agent_start", "role": role.value}
|
|
@@ -164,28 +235,64 @@ class Orchestrator:
|
|
| 164 |
if msg.role.value in allowed_roles:
|
| 165 |
filtered_history.append(msg)
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
"content_length": len(response.content),
|
| 182 |
-
}
|
| 183 |
|
| 184 |
-
if role in self.judged_roles:
|
| 185 |
yield {"type": "judge_start", "role": role.value}
|
| 186 |
|
| 187 |
judge_output = await self.agent_system.evaluate_step(
|
| 188 |
-
role=role, content=response.content, context=
|
| 189 |
)
|
| 190 |
|
| 191 |
all_judge_results[role.value] = {
|
|
@@ -201,167 +308,225 @@ class Orchestrator:
|
|
| 201 |
"role": role.value,
|
| 202 |
"is_approved": judge_output.is_approved,
|
| 203 |
"score": judge_output.score,
|
|
|
|
| 204 |
}
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
yield {
|
| 207 |
"type": "pipeline_complete",
|
| 208 |
-
"
|
| 209 |
-
"
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
def _build_graph(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
graph = StateGraph(AgentState)
|
| 214 |
|
| 215 |
-
#
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
# 2. Add All Agent Nodes
|
| 219 |
-
all_roles = [
|
| 220 |
-
TeamRole.PROJECT_REFINER,
|
| 221 |
-
TeamRole.PRODUCT_OWNER,
|
| 222 |
-
# Phase 2
|
| 223 |
TeamRole.BUSINESS_ANALYST,
|
| 224 |
TeamRole.SOLUTION_ARCHITECT,
|
| 225 |
TeamRole.DATA_ARCHITECT,
|
| 226 |
TeamRole.SECURITY_ANALYST,
|
| 227 |
-
|
|
|
|
| 228 |
TeamRole.UX_DESIGNER,
|
| 229 |
TeamRole.API_DESIGNER,
|
| 230 |
TeamRole.QA_STRATEGIST,
|
| 231 |
TeamRole.DEVOPS_ARCHITECT,
|
| 232 |
-
|
|
|
|
| 233 |
TeamRole.ENVIRONMENT_ENGINEER,
|
| 234 |
TeamRole.TECHNICAL_WRITER,
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
for role in all_roles:
|
| 240 |
graph.add_node(role.value, self._make_agent_node(role))
|
| 241 |
|
| 242 |
-
# Add Judge Node
|
| 243 |
if role in self.judged_roles:
|
| 244 |
judge_node_name = f"judge_{role.value}"
|
| 245 |
graph.add_node(judge_node_name, self._make_judge_node(role))
|
| 246 |
|
| 247 |
-
# 3.
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
-
#
|
| 250 |
-
graph.
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
graph.add_edge("gather_context", TeamRole.PROJECT_REFINER.value)
|
| 254 |
|
| 255 |
-
#
|
| 256 |
-
graph.add_edge(TeamRole.PROJECT_REFINER.value, TeamRole.PRODUCT_OWNER.value)
|
| 257 |
|
| 258 |
-
#
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
TeamRole.PRODUCT_OWNER,
|
| 262 |
-
[
|
| 263 |
-
TeamRole.BUSINESS_ANALYST,
|
| 264 |
-
TeamRole.SOLUTION_ARCHITECT,
|
| 265 |
-
TeamRole.DATA_ARCHITECT,
|
| 266 |
-
TeamRole.SECURITY_ANALYST,
|
| 267 |
-
],
|
| 268 |
-
)
|
| 269 |
|
| 270 |
-
#
|
| 271 |
-
graph.
|
| 272 |
-
for r in [
|
| 273 |
-
TeamRole.BUSINESS_ANALYST,
|
| 274 |
-
TeamRole.SOLUTION_ARCHITECT,
|
| 275 |
-
TeamRole.DATA_ARCHITECT,
|
| 276 |
-
TeamRole.SECURITY_ANALYST,
|
| 277 |
-
]:
|
| 278 |
-
self._add_role_edges(graph, r, ["phase_2_gate"])
|
| 279 |
|
| 280 |
-
#
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
# Phase
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
-
# Phase 3
|
| 300 |
-
for r in
|
| 301 |
-
graph.add_edge(
|
| 302 |
|
| 303 |
-
#
|
| 304 |
-
graph.
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
-
# Phase 4
|
| 309 |
-
|
|
|
|
| 310 |
|
| 311 |
-
# Spec Coordinator
|
| 312 |
-
graph.add_edge(
|
|
|
|
| 313 |
|
| 314 |
return graph.compile()
|
| 315 |
|
| 316 |
-
def
|
| 317 |
-
"""
|
| 318 |
-
|
| 319 |
-
"""
|
| 320 |
-
source = role.value
|
| 321 |
-
|
| 322 |
-
# Resolve destination value helper
|
| 323 |
-
def get_dest_value(n):
|
| 324 |
-
return n.value if isinstance(n, TeamRole) else n
|
| 325 |
-
|
| 326 |
-
if role in self.judged_roles:
|
| 327 |
-
judge = f"judge_{role.value}"
|
| 328 |
-
graph.add_edge(source, judge)
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
|
|
|
| 332 |
|
| 333 |
-
|
| 334 |
-
judge,
|
| 335 |
-
self._should_continue(role),
|
| 336 |
-
{"continue": success_dest, "retry": source, "abort": END},
|
| 337 |
-
)
|
| 338 |
-
else:
|
| 339 |
-
# Direct edges
|
| 340 |
-
for target in next_nodes:
|
| 341 |
-
graph.add_edge(source, get_dest_value(target))
|
| 342 |
|
| 343 |
def _make_gate_node(self, name: str):
|
| 344 |
def node(state: AgentState) -> dict:
|
| 345 |
-
|
| 346 |
return {} # Pass through
|
| 347 |
|
| 348 |
return node
|
| 349 |
|
| 350 |
def _make_context_node(self):
|
| 351 |
def node(state: AgentState) -> dict:
|
| 352 |
-
|
| 353 |
query = state["context"]
|
| 354 |
docs = self.rag_service.retrieve(query, k=3)
|
| 355 |
retrieval_context = self.rag_service.format_docs(docs)
|
| 356 |
-
|
| 357 |
return {"retrieval_context": retrieval_context}
|
| 358 |
|
| 359 |
return node
|
| 360 |
|
| 361 |
def _make_agent_node(self, role: TeamRole):
|
| 362 |
async def node(state: AgentState) -> dict:
|
| 363 |
-
|
| 364 |
-
f"Running agent: {role.value} (
|
| 365 |
)
|
| 366 |
|
| 367 |
# 1. Filter Context based on dependencies
|
|
@@ -388,16 +553,30 @@ class Orchestrator:
|
|
| 388 |
else ""
|
| 389 |
)
|
| 390 |
|
| 391 |
-
# 3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
response = await self.agent_system.process_step(
|
| 393 |
role=role,
|
| 394 |
-
context=
|
| 395 |
previous_outputs=filtered_history,
|
| 396 |
feedback=feedback,
|
| 397 |
retrieval_context=state.get("retrieval_context", ""),
|
| 398 |
)
|
| 399 |
|
| 400 |
-
#
|
| 401 |
return {
|
| 402 |
"history": [response],
|
| 403 |
"outputs": {role.value: response.content},
|
|
@@ -408,15 +587,16 @@ class Orchestrator:
|
|
| 408 |
|
| 409 |
def _make_judge_node(self, role: TeamRole):
|
| 410 |
async def node(state: AgentState) -> dict:
|
| 411 |
-
|
| 412 |
latest_output = state["outputs"].get(role.value, "")
|
| 413 |
|
| 414 |
judge_output: JudgeOutput = await self.agent_system.evaluate_step(
|
| 415 |
role=role, content=latest_output, context=state["context"]
|
| 416 |
)
|
| 417 |
|
| 418 |
-
|
| 419 |
-
f"Judge decision
|
|
|
|
| 420 |
)
|
| 421 |
|
| 422 |
# Store judge result
|
|
@@ -460,7 +640,10 @@ class Orchestrator:
|
|
| 460 |
return "continue"
|
| 461 |
|
| 462 |
if retry_count >= self.max_retries:
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
| 464 |
return "continue"
|
| 465 |
|
| 466 |
return "retry"
|
|
|
|
| 1 |
+
import logging
|
| 2 |
from collections.abc import AsyncIterator
|
| 3 |
from typing import Annotated, Any, Literal, TypedDict
|
| 4 |
|
| 5 |
+
from langgraph.constants import END, Send
|
| 6 |
+
from langgraph.graph import StateGraph
|
| 7 |
|
| 8 |
from .agents import AgentSystem
|
| 9 |
from .rag import RAGService
|
| 10 |
from .schemas import AgentResponse, JudgeOutput, ProjectRequest, TeamRole
|
| 11 |
|
| 12 |
+
logger = logging.getLogger("orchestrator")
|
| 13 |
+
|
| 14 |
|
| 15 |
def merge_dicts(a: dict, b: dict) -> dict:
|
| 16 |
return {**a, **b}
|
|
|
|
| 34 |
feedback: Annotated[str, replace_reducer]
|
| 35 |
retry_count: Annotated[int, replace_reducer]
|
| 36 |
judge_results: Annotated[dict[str, dict[str, Any]], merge_dicts]
|
| 37 |
+
prd_context: Annotated[dict[str, Any], replace_reducer]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# PRD context mapping - which PRD sections each agent should receive
|
| 41 |
+
PRD_CONTEXT_FOR_ROLE: dict[TeamRole, list[str]] = {
|
| 42 |
+
TeamRole.BUSINESS_ANALYST: ["user_stories", "features"],
|
| 43 |
+
TeamRole.SOLUTION_ARCHITECT: ["features", "assumptions"],
|
| 44 |
+
TeamRole.DATA_ARCHITECT: ["user_stories", "features"],
|
| 45 |
+
TeamRole.SECURITY_ANALYST: ["features", "assumptions"],
|
| 46 |
+
TeamRole.UX_DESIGNER: ["user_stories"],
|
| 47 |
+
TeamRole.API_DESIGNER: ["user_stories", "features"],
|
| 48 |
+
TeamRole.QA_STRATEGIST: ["user_stories", "features"],
|
| 49 |
+
TeamRole.DEVOPS_ARCHITECT: ["features", "assumptions"],
|
| 50 |
+
TeamRole.TECHNICAL_WRITER: [
|
| 51 |
+
"product_vision",
|
| 52 |
+
"features",
|
| 53 |
+
"user_stories",
|
| 54 |
+
"assumptions",
|
| 55 |
+
],
|
| 56 |
+
}
|
| 57 |
|
| 58 |
|
| 59 |
# Dependency mapping
|
| 60 |
+
ALL_DEPENDENCIES: list[TeamRole] = list(TeamRole)
|
| 61 |
+
AGENT_DEPENDENCIES: dict[TeamRole, list[TeamRole]] = {
|
| 62 |
TeamRole.PROJECT_REFINER: [],
|
| 63 |
TeamRole.PRODUCT_OWNER: [TeamRole.PROJECT_REFINER],
|
| 64 |
# Phase 2
|
|
|
|
| 78 |
],
|
| 79 |
TeamRole.TECHNICAL_WRITER: [TeamRole.PRODUCT_OWNER], # Needs overview
|
| 80 |
# Phase 5
|
| 81 |
+
TeamRole.SPEC_COORDINATOR: ALL_DEPENDENCIES, # All inputs
|
| 82 |
}
|
| 83 |
|
| 84 |
|
|
|
|
| 97 |
self.max_retries = 2
|
| 98 |
self.graph = self._build_graph()
|
| 99 |
|
| 100 |
+
async def run_pipeline(
|
| 101 |
+
self, project_request: ProjectRequest, prd_context: dict | None = None
|
| 102 |
+
) -> dict[str, Any]:
|
| 103 |
"""
|
| 104 |
Run the full multi-agent pipeline.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
project_request: The project request
|
| 108 |
+
prd_context: Optional PRD context (if PRD was generated first)
|
| 109 |
"""
|
| 110 |
+
has_prd = prd_context is not None
|
| 111 |
+
prd_ctx = prd_context or {}
|
| 112 |
+
|
| 113 |
+
if has_prd:
|
| 114 |
+
initial_context = (
|
| 115 |
+
f"PRD Input:\n{prd_ctx.get('full_text', project_request.description)}"
|
| 116 |
+
)
|
| 117 |
+
else:
|
| 118 |
+
initial_context = f"Project Description: {project_request.description}"
|
| 119 |
|
| 120 |
initial_state: AgentState = {
|
| 121 |
"context": initial_context,
|
|
|
|
| 126 |
"feedback": "",
|
| 127 |
"retry_count": 0,
|
| 128 |
"judge_results": {},
|
| 129 |
+
"prd_context": prd_ctx,
|
| 130 |
}
|
| 131 |
|
| 132 |
final_state = await self.graph.ainvoke(initial_state)
|
| 133 |
|
| 134 |
+
# Get the final SRS from spec_coordinator
|
| 135 |
+
all_outputs = final_state.get("outputs", {})
|
| 136 |
+
final_srs = all_outputs.get(TeamRole.SPEC_COORDINATOR.value, "")
|
| 137 |
+
|
| 138 |
return {
|
| 139 |
+
"srs_document": final_srs, # Clean SRS for user
|
| 140 |
+
"markdown_outputs": all_outputs, # Internal use - all agent outputs
|
| 141 |
+
"judge_results": final_state.get("judge_results", {}), # Internal use
|
| 142 |
+
"prd_used": has_prd,
|
| 143 |
}
|
| 144 |
|
| 145 |
async def run_pipeline_streaming(
|
| 146 |
+
self, project_request: ProjectRequest, prd_context: dict | None = None
|
| 147 |
) -> AsyncIterator[dict[str, Any]]:
|
| 148 |
"""
|
| 149 |
Run the pipeline with streaming output for each agent.
|
|
|
|
| 159 |
- pipeline_complete: Full pipeline finished with results
|
| 160 |
- error: Error occurred
|
| 161 |
"""
|
| 162 |
+
has_prd = prd_context is not None
|
| 163 |
+
prd_ctx = prd_context or {}
|
| 164 |
|
| 165 |
+
if has_prd:
|
| 166 |
+
initial_context = (
|
| 167 |
+
f"PRD Input:\n{prd_ctx.get('full_text', project_request.description)}"
|
| 168 |
+
)
|
| 169 |
+
yield {
|
| 170 |
+
"type": "status",
|
| 171 |
+
"message": "Starting Multi-Agent Pipeline with PRD input...",
|
| 172 |
+
}
|
| 173 |
+
else:
|
| 174 |
+
initial_context = f"Project Description: {project_request.description}"
|
| 175 |
+
yield {"type": "status", "message": "Starting Multi-Agent Pipeline..."}
|
| 176 |
|
| 177 |
query = initial_context
|
| 178 |
docs = self.rag_service.retrieve(query, k=3)
|
|
|
|
| 187 |
all_judge_results: dict[str, dict[str, Any]] = {}
|
| 188 |
history: list[AgentResponse] = []
|
| 189 |
|
| 190 |
+
if has_prd:
|
| 191 |
+
execution_order = [
|
| 192 |
+
TeamRole.BUSINESS_ANALYST,
|
| 193 |
+
TeamRole.SOLUTION_ARCHITECT,
|
| 194 |
+
TeamRole.DATA_ARCHITECT,
|
| 195 |
+
TeamRole.SECURITY_ANALYST,
|
| 196 |
+
TeamRole.UX_DESIGNER,
|
| 197 |
+
TeamRole.API_DESIGNER,
|
| 198 |
+
TeamRole.QA_STRATEGIST,
|
| 199 |
+
TeamRole.DEVOPS_ARCHITECT,
|
| 200 |
+
TeamRole.ENVIRONMENT_ENGINEER,
|
| 201 |
+
TeamRole.TECHNICAL_WRITER,
|
| 202 |
+
TeamRole.SPEC_COORDINATOR,
|
| 203 |
+
]
|
| 204 |
+
else:
|
| 205 |
+
execution_order = [
|
| 206 |
+
TeamRole.PROJECT_REFINER,
|
| 207 |
+
TeamRole.PRODUCT_OWNER,
|
| 208 |
+
TeamRole.BUSINESS_ANALYST,
|
| 209 |
+
TeamRole.SOLUTION_ARCHITECT,
|
| 210 |
+
TeamRole.DATA_ARCHITECT,
|
| 211 |
+
TeamRole.SECURITY_ANALYST,
|
| 212 |
+
TeamRole.UX_DESIGNER,
|
| 213 |
+
TeamRole.API_DESIGNER,
|
| 214 |
+
TeamRole.QA_STRATEGIST,
|
| 215 |
+
TeamRole.DEVOPS_ARCHITECT,
|
| 216 |
+
TeamRole.ENVIRONMENT_ENGINEER,
|
| 217 |
+
TeamRole.TECHNICAL_WRITER,
|
| 218 |
+
TeamRole.SPEC_COORDINATOR,
|
| 219 |
+
]
|
| 220 |
|
| 221 |
for role in execution_order:
|
| 222 |
yield {"type": "agent_start", "role": role.value}
|
|
|
|
| 235 |
if msg.role.value in allowed_roles:
|
| 236 |
filtered_history.append(msg)
|
| 237 |
|
| 238 |
+
agent_context = initial_context
|
| 239 |
+
if has_prd and role in PRD_CONTEXT_FOR_ROLE:
|
| 240 |
+
prd_sections = PRD_CONTEXT_FOR_ROLE[role]
|
| 241 |
+
prd_context_text = "\n\n## PRD Context for this Agent:\n"
|
| 242 |
+
for section in prd_sections:
|
| 243 |
+
if section in prd_ctx:
|
| 244 |
+
prd_context_text += (
|
| 245 |
+
f"\n### {section.replace('_', ' ').title()}:\n"
|
| 246 |
+
)
|
| 247 |
+
prd_context_text += f"{prd_ctx[section]}\n"
|
| 248 |
+
agent_context = prd_context_text + "\n\n---\n\n" + initial_context
|
| 249 |
+
|
| 250 |
+
# --- Agent execution with judge retry loop ---
|
| 251 |
+
feedback = ""
|
| 252 |
+
response: AgentResponse | None = None
|
| 253 |
+
for attempt in range(self.max_retries + 1):
|
| 254 |
+
if attempt > 0:
|
| 255 |
+
logger.info(
|
| 256 |
+
f"Retry {attempt}/{self.max_retries} for {role.value} "
|
| 257 |
+
f"after judge rejection"
|
| 258 |
+
)
|
| 259 |
+
yield {
|
| 260 |
+
"type": "agent_start",
|
| 261 |
+
"role": role.value,
|
| 262 |
+
"retry": attempt,
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
# Stream agent output
|
| 266 |
+
full_content = ""
|
| 267 |
+
async for chunk in self.agent_system.process_step_streaming(
|
| 268 |
+
role=role,
|
| 269 |
+
context=agent_context,
|
| 270 |
+
previous_outputs=filtered_history,
|
| 271 |
+
feedback=feedback,
|
| 272 |
+
retrieval_context=retrieval_context,
|
| 273 |
+
):
|
| 274 |
+
full_content += chunk
|
| 275 |
+
yield {"type": "chunk", "role": role.value, "chunk": chunk}
|
| 276 |
+
|
| 277 |
+
# Create response from accumulated content
|
| 278 |
+
response = AgentResponse(
|
| 279 |
+
role=role, content=full_content, metadata={"format": "markdown"}
|
| 280 |
+
)
|
| 281 |
|
| 282 |
+
yield {
|
| 283 |
+
"type": "agent_complete",
|
| 284 |
+
"role": role.value,
|
| 285 |
+
"content_length": len(response.content),
|
| 286 |
+
}
|
| 287 |
|
| 288 |
+
# --- Judge evaluation (only for judged roles) ---
|
| 289 |
+
if role not in self.judged_roles:
|
| 290 |
+
break # No judge, accept immediately
|
|
|
|
|
|
|
| 291 |
|
|
|
|
| 292 |
yield {"type": "judge_start", "role": role.value}
|
| 293 |
|
| 294 |
judge_output = await self.agent_system.evaluate_step(
|
| 295 |
+
role=role, content=response.content, context=agent_context
|
| 296 |
)
|
| 297 |
|
| 298 |
all_judge_results[role.value] = {
|
|
|
|
| 308 |
"role": role.value,
|
| 309 |
"is_approved": judge_output.is_approved,
|
| 310 |
"score": judge_output.score,
|
| 311 |
+
"attempt": attempt + 1,
|
| 312 |
}
|
| 313 |
|
| 314 |
+
if (
|
| 315 |
+
judge_output.is_approved
|
| 316 |
+
or judge_output.recommended_action == "accept"
|
| 317 |
+
):
|
| 318 |
+
logger.info(
|
| 319 |
+
f"Judge approved {role.value} "
|
| 320 |
+
f"(score={judge_output.score}, attempt={attempt + 1})"
|
| 321 |
+
)
|
| 322 |
+
break # Approved — move on
|
| 323 |
+
|
| 324 |
+
# Build detailed feedback for retry
|
| 325 |
+
feedback = judge_output.feedback or ""
|
| 326 |
+
if judge_output.issues:
|
| 327 |
+
feedback += "\n\n**Specific Issues:**\n"
|
| 328 |
+
for issue in judge_output.issues:
|
| 329 |
+
feedback += f"- {issue.suggestion}\n"
|
| 330 |
+
|
| 331 |
+
if attempt < self.max_retries:
|
| 332 |
+
logger.info(
|
| 333 |
+
f"Judge rejected {role.value} "
|
| 334 |
+
f"(score={judge_output.score}), retrying..."
|
| 335 |
+
)
|
| 336 |
+
else:
|
| 337 |
+
logger.warning(
|
| 338 |
+
f"Max retries ({self.max_retries}) reached for {role.value}, "
|
| 339 |
+
f"proceeding with last output (score={judge_output.score})"
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# Store final result (whether approved or max-retried)
|
| 343 |
+
if response is None:
|
| 344 |
+
response = AgentResponse(
|
| 345 |
+
role=role, content="", metadata={"format": "markdown"}
|
| 346 |
+
)
|
| 347 |
+
history.append(response)
|
| 348 |
+
all_outputs[role.value] = response.content
|
| 349 |
+
|
| 350 |
+
# Get the final SRS from spec_coordinator
|
| 351 |
+
final_srs = all_outputs.get(TeamRole.SPEC_COORDINATOR.value, "")
|
| 352 |
+
|
| 353 |
yield {
|
| 354 |
"type": "pipeline_complete",
|
| 355 |
+
"srs_document": final_srs, # Clean SRS for user
|
| 356 |
+
"markdown_outputs": all_outputs, # Internal use - all agent outputs
|
| 357 |
+
"judge_results": all_judge_results, # Internal use - judge feedback
|
| 358 |
+
"prd_used": has_prd,
|
| 359 |
}
|
| 360 |
|
| 361 |
def _build_graph(self):
|
| 362 |
+
"""
|
| 363 |
+
Build a graph with TRUE parallel execution using LangGraph's Send API.
|
| 364 |
+
|
| 365 |
+
Phase structure:
|
| 366 |
+
- Phase 1: Sequential (Refiner -> Product Owner)
|
| 367 |
+
- Phase 2: Parallel (BA, SA, DA, Security)
|
| 368 |
+
- Phase 3: Parallel (UX, API, QA, DevOps)
|
| 369 |
+
- Phase 4: Parallel (Env Engineer, Tech Writer)
|
| 370 |
+
- Phase 5: Sequential (Spec Coordinator)
|
| 371 |
+
"""
|
| 372 |
graph = StateGraph(AgentState)
|
| 373 |
|
| 374 |
+
# Define phases
|
| 375 |
+
PHASE_1 = [TeamRole.PROJECT_REFINER, TeamRole.PRODUCT_OWNER] # Sequential
|
| 376 |
+
PHASE_2 = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
TeamRole.BUSINESS_ANALYST,
|
| 378 |
TeamRole.SOLUTION_ARCHITECT,
|
| 379 |
TeamRole.DATA_ARCHITECT,
|
| 380 |
TeamRole.SECURITY_ANALYST,
|
| 381 |
+
] # Parallel
|
| 382 |
+
PHASE_3 = [
|
| 383 |
TeamRole.UX_DESIGNER,
|
| 384 |
TeamRole.API_DESIGNER,
|
| 385 |
TeamRole.QA_STRATEGIST,
|
| 386 |
TeamRole.DEVOPS_ARCHITECT,
|
| 387 |
+
] # Parallel
|
| 388 |
+
PHASE_4 = [
|
| 389 |
TeamRole.ENVIRONMENT_ENGINEER,
|
| 390 |
TeamRole.TECHNICAL_WRITER,
|
| 391 |
+
] # Parallel
|
| 392 |
+
PHASE_5 = [TeamRole.SPEC_COORDINATOR] # Sequential (aggregation)
|
| 393 |
+
|
| 394 |
+
# 1. Add Context Node
|
| 395 |
+
graph.add_node("gather_context", self._make_context_node())
|
| 396 |
+
|
| 397 |
+
# 2. Add All Agent Nodes
|
| 398 |
+
all_roles = PHASE_1 + PHASE_2 + PHASE_3 + PHASE_4 + PHASE_5
|
| 399 |
|
| 400 |
for role in all_roles:
|
| 401 |
graph.add_node(role.value, self._make_agent_node(role))
|
| 402 |
|
| 403 |
+
# Add Judge Node for judged roles
|
| 404 |
if role in self.judged_roles:
|
| 405 |
judge_node_name = f"judge_{role.value}"
|
| 406 |
graph.add_node(judge_node_name, self._make_judge_node(role))
|
| 407 |
|
| 408 |
+
# 3. Add Fan-out Nodes for Parallel Execution (one per phase)
|
| 409 |
+
graph.add_node("fanout_phase_2", self._make_fanout_node(PHASE_2))
|
| 410 |
+
graph.add_node("fanout_phase_3", self._make_fanout_node(PHASE_3))
|
| 411 |
+
graph.add_node("fanout_phase_4", self._make_fanout_node(PHASE_4))
|
| 412 |
|
| 413 |
+
# 4. Add Gate Nodes for synchronization
|
| 414 |
+
graph.add_node("phase_2_gate", self._make_gate_node("Phase 2"))
|
| 415 |
+
graph.add_node("phase_3_gate", self._make_gate_node("Phase 3"))
|
| 416 |
+
graph.add_node("phase_4_gate", self._make_gate_node("Phase 4"))
|
|
|
|
| 417 |
|
| 418 |
+
# 5. Define Flow with Parallel Execution
|
|
|
|
| 419 |
|
| 420 |
+
# === PHASE 1: Sequential ===
|
| 421 |
+
graph.set_entry_point("gather_context")
|
| 422 |
+
graph.add_edge("gather_context", PHASE_1[0].value) # Refiner
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
+
# Refiner -> Product Owner (sequential)
|
| 425 |
+
graph.add_edge(PHASE_1[0].value, PHASE_1[1].value) # Refiner -> PO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
+
# PO -> Judge (if applicable) -> Phase 2 fanout
|
| 428 |
+
if TeamRole.PRODUCT_OWNER in self.judged_roles:
|
| 429 |
+
graph.add_edge(PHASE_1[1].value, f"judge_{TeamRole.PRODUCT_OWNER.value}")
|
| 430 |
+
graph.add_conditional_edges(
|
| 431 |
+
f"judge_{TeamRole.PRODUCT_OWNER.value}",
|
| 432 |
+
self._should_continue(TeamRole.PRODUCT_OWNER),
|
| 433 |
+
{
|
| 434 |
+
"continue": "fanout_phase_2",
|
| 435 |
+
"retry": PHASE_1[1].value,
|
| 436 |
+
"abort": END,
|
| 437 |
+
},
|
| 438 |
+
)
|
| 439 |
+
else:
|
| 440 |
+
graph.add_edge(PHASE_1[1].value, "fanout_phase_2")
|
| 441 |
+
|
| 442 |
+
# === PHASE 2: Parallel using Send ===
|
| 443 |
+
# Fanout node sends to all Phase 2 agents in parallel
|
| 444 |
+
graph.add_conditional_edges(
|
| 445 |
+
"fanout_phase_2",
|
| 446 |
+
lambda s: [Send(r.value, s) for r in PHASE_2],
|
| 447 |
+
[r.value for r in PHASE_2],
|
| 448 |
+
)
|
| 449 |
|
| 450 |
+
# Phase 2 agents -> Judge (if applicable) -> Phase 2 Gate
|
| 451 |
+
for r in PHASE_2:
|
| 452 |
+
if r in self.judged_roles:
|
| 453 |
+
# Judged role: agent -> judge -> (continue|retry|abort)
|
| 454 |
+
graph.add_edge(r.value, f"judge_{r.value}")
|
| 455 |
+
graph.add_conditional_edges(
|
| 456 |
+
f"judge_{r.value}",
|
| 457 |
+
self._should_continue(r),
|
| 458 |
+
{
|
| 459 |
+
"continue": "phase_2_gate",
|
| 460 |
+
"retry": r.value,
|
| 461 |
+
"abort": END,
|
| 462 |
+
},
|
| 463 |
+
)
|
| 464 |
+
else:
|
| 465 |
+
# Non-judged role: agent -> gate directly
|
| 466 |
+
graph.add_edge(r.value, "phase_2_gate")
|
| 467 |
+
|
| 468 |
+
# === PHASE 3: Parallel using Send ===
|
| 469 |
+
graph.add_edge("phase_2_gate", "fanout_phase_3")
|
| 470 |
+
graph.add_conditional_edges(
|
| 471 |
+
"fanout_phase_3",
|
| 472 |
+
lambda s: [Send(r.value, s) for r in PHASE_3],
|
| 473 |
+
[r.value for r in PHASE_3],
|
| 474 |
+
)
|
| 475 |
|
| 476 |
+
# Phase 3 agents -> Phase 3 Gate
|
| 477 |
+
for r in PHASE_3:
|
| 478 |
+
graph.add_edge(r.value, "phase_3_gate")
|
| 479 |
|
| 480 |
+
# === PHASE 4: Parallel using Send ===
|
| 481 |
+
graph.add_edge("phase_3_gate", "fanout_phase_4")
|
| 482 |
+
graph.add_conditional_edges(
|
| 483 |
+
"fanout_phase_4",
|
| 484 |
+
lambda s: [Send(r.value, s) for r in PHASE_4],
|
| 485 |
+
[r.value for r in PHASE_4],
|
| 486 |
+
)
|
| 487 |
|
| 488 |
+
# Phase 4 agents -> Phase 4 Gate
|
| 489 |
+
for r in PHASE_4:
|
| 490 |
+
graph.add_edge(r.value, "phase_4_gate")
|
| 491 |
|
| 492 |
+
# === PHASE 5: Sequential (Spec Coordinator) ===
|
| 493 |
+
graph.add_edge("phase_4_gate", PHASE_5[0].value)
|
| 494 |
+
graph.add_edge(PHASE_5[0].value, END)
|
| 495 |
|
| 496 |
return graph.compile()
|
| 497 |
|
| 498 |
+
def _make_fanout_node(self, roles: list[TeamRole]):
|
| 499 |
+
"""Create a fanout node that sends to parallel agents."""
|
| 500 |
+
_ = roles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
|
| 502 |
+
def node(state: AgentState) -> dict:
|
| 503 |
+
# This node just passes through - the conditional edge does the Send
|
| 504 |
+
return {}
|
| 505 |
|
| 506 |
+
return node
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
def _make_gate_node(self, name: str):
|
| 509 |
def node(state: AgentState) -> dict:
|
| 510 |
+
logger.info(f"Reached gate: {name}")
|
| 511 |
return {} # Pass through
|
| 512 |
|
| 513 |
return node
|
| 514 |
|
| 515 |
def _make_context_node(self):
|
| 516 |
def node(state: AgentState) -> dict:
|
| 517 |
+
logger.info("Gathering context from RAG...")
|
| 518 |
query = state["context"]
|
| 519 |
docs = self.rag_service.retrieve(query, k=3)
|
| 520 |
retrieval_context = self.rag_service.format_docs(docs)
|
| 521 |
+
logger.info(f"Retrieved {len(docs)} documents from RAG")
|
| 522 |
return {"retrieval_context": retrieval_context}
|
| 523 |
|
| 524 |
return node
|
| 525 |
|
| 526 |
def _make_agent_node(self, role: TeamRole):
|
| 527 |
async def node(state: AgentState) -> dict:
|
| 528 |
+
logger.info(
|
| 529 |
+
f"Running agent: {role.value} (retry={state.get('retry_count', 0)})"
|
| 530 |
)
|
| 531 |
|
| 532 |
# 1. Filter Context based on dependencies
|
|
|
|
| 553 |
else ""
|
| 554 |
)
|
| 555 |
|
| 556 |
+
# 3. Inject PRD context per role (matching streaming pipeline behaviour)
|
| 557 |
+
agent_context = state["context"]
|
| 558 |
+
prd_ctx = state.get("prd_context") or {}
|
| 559 |
+
if prd_ctx and role in PRD_CONTEXT_FOR_ROLE:
|
| 560 |
+
prd_sections = PRD_CONTEXT_FOR_ROLE[role]
|
| 561 |
+
prd_context_text = "\n\n## PRD Context for this Agent:\n"
|
| 562 |
+
for section in prd_sections:
|
| 563 |
+
if section in prd_ctx:
|
| 564 |
+
prd_context_text += (
|
| 565 |
+
f"\n### {section.replace('_', ' ').title()}:\n"
|
| 566 |
+
)
|
| 567 |
+
prd_context_text += f"{prd_ctx[section]}\n"
|
| 568 |
+
agent_context = prd_context_text + "\n\n---\n\n" + agent_context
|
| 569 |
+
|
| 570 |
+
# 4. Process
|
| 571 |
response = await self.agent_system.process_step(
|
| 572 |
role=role,
|
| 573 |
+
context=agent_context,
|
| 574 |
previous_outputs=filtered_history,
|
| 575 |
feedback=feedback,
|
| 576 |
retrieval_context=state.get("retrieval_context", ""),
|
| 577 |
)
|
| 578 |
|
| 579 |
+
# 5. Return update (merged by Annotated)
|
| 580 |
return {
|
| 581 |
"history": [response],
|
| 582 |
"outputs": {role.value: response.content},
|
|
|
|
| 587 |
|
| 588 |
def _make_judge_node(self, role: TeamRole):
|
| 589 |
async def node(state: AgentState) -> dict:
|
| 590 |
+
logger.info(f"Running judge for: {role.value}")
|
| 591 |
latest_output = state["outputs"].get(role.value, "")
|
| 592 |
|
| 593 |
judge_output: JudgeOutput = await self.agent_system.evaluate_step(
|
| 594 |
role=role, content=latest_output, context=state["context"]
|
| 595 |
)
|
| 596 |
|
| 597 |
+
logger.info(
|
| 598 |
+
f"Judge decision for {role.value}: "
|
| 599 |
+
f"approved={judge_output.is_approved}, score={judge_output.score}"
|
| 600 |
)
|
| 601 |
|
| 602 |
# Store judge result
|
|
|
|
| 640 |
return "continue"
|
| 641 |
|
| 642 |
if retry_count >= self.max_retries:
|
| 643 |
+
logger.warning(
|
| 644 |
+
f"Max retries ({self.max_retries}) reached for {role.value}, "
|
| 645 |
+
f"continuing with current output"
|
| 646 |
+
)
|
| 647 |
return "continue"
|
| 648 |
|
| 649 |
return "retry"
|
app/core/rag.py
CHANGED
|
@@ -199,7 +199,7 @@ class RAGService:
|
|
| 199 |
search_kwargs=search_kwargs,
|
| 200 |
)
|
| 201 |
|
| 202 |
-
|
| 203 |
|
| 204 |
def format_docs(self, docs: list[Document]) -> str:
|
| 205 |
"""
|
|
|
|
| 199 |
search_kwargs=search_kwargs,
|
| 200 |
)
|
| 201 |
|
| 202 |
+
raise RuntimeError("RAG retriever not initialized")
|
| 203 |
|
| 204 |
def format_docs(self, docs: list[Document]) -> str:
|
| 205 |
"""
|
app/core/resilience.py
CHANGED
|
@@ -81,6 +81,7 @@ class CircuitBreaker:
|
|
| 81 |
self.half_open_calls += 1
|
| 82 |
return True
|
| 83 |
return False
|
|
|
|
| 84 |
|
| 85 |
async def record_success(self):
|
| 86 |
"""Record a successful call."""
|
|
@@ -161,14 +162,14 @@ class RetryConfig:
|
|
| 161 |
|
| 162 |
async def retry_with_backoff[T](
|
| 163 |
func: Callable[..., Awaitable[T]],
|
| 164 |
-
*args,
|
| 165 |
config: RetryConfig | None = None,
|
| 166 |
-
retryable_exceptions: tuple = (Exception,),
|
| 167 |
-
**kwargs,
|
| 168 |
) -> T:
|
| 169 |
"""Execute a function with exponential backoff retry logic."""
|
| 170 |
config = config or RetryConfig()
|
| 171 |
-
last_exception:
|
| 172 |
for attempt in range(config.max_retries + 1):
|
| 173 |
try:
|
| 174 |
return await func(*args, **kwargs)
|
|
@@ -189,15 +190,22 @@ async def retry_with_backoff[T](
|
|
| 189 |
)
|
| 190 |
await asyncio.sleep(delay)
|
| 191 |
if last_exception:
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
|
| 195 |
-
def with_circuit_breaker(
|
|
|
|
|
|
|
| 196 |
"""Decorator to protect a function with a circuit breaker."""
|
| 197 |
|
| 198 |
-
def decorator(
|
|
|
|
|
|
|
| 199 |
@wraps(func)
|
| 200 |
-
async def wrapper(*args, **kwargs) -> T:
|
| 201 |
circuit = get_circuit_breaker(circuit_name)
|
| 202 |
if not await circuit.can_execute():
|
| 203 |
raise CircuitOpenError(
|
|
@@ -219,13 +227,15 @@ def with_circuit_breaker(circuit_name: str):
|
|
| 219 |
def with_retry(
|
| 220 |
max_retries: int = 3,
|
| 221 |
base_delay: float = 1.0,
|
| 222 |
-
retryable_exceptions: tuple = (Exception,),
|
| 223 |
-
):
|
| 224 |
"""Decorator to add retry logic with exponential backoff."""
|
| 225 |
|
| 226 |
-
def decorator(
|
|
|
|
|
|
|
| 227 |
@wraps(func)
|
| 228 |
-
async def wrapper(*args, **kwargs) -> T:
|
| 229 |
config = RetryConfig(max_retries=max_retries, base_delay=base_delay)
|
| 230 |
return await retry_with_backoff(
|
| 231 |
func,
|
|
@@ -350,7 +360,10 @@ class GracefulDegradation:
|
|
| 350 |
|
| 351 |
@staticmethod
|
| 352 |
async def with_fallback(
|
| 353 |
-
primary_func: Callable[...,
|
|
|
|
|
|
|
|
|
|
| 354 |
) -> T:
|
| 355 |
"""Execute primary function with fallback on failure."""
|
| 356 |
try:
|
|
|
|
| 81 |
self.half_open_calls += 1
|
| 82 |
return True
|
| 83 |
return False
|
| 84 |
+
return False
|
| 85 |
|
| 86 |
async def record_success(self):
|
| 87 |
"""Record a successful call."""
|
|
|
|
| 162 |
|
| 163 |
async def retry_with_backoff[T](
|
| 164 |
func: Callable[..., Awaitable[T]],
|
| 165 |
+
*args: Any,
|
| 166 |
config: RetryConfig | None = None,
|
| 167 |
+
retryable_exceptions: tuple[type[BaseException], ...] = (Exception,),
|
| 168 |
+
**kwargs: Any,
|
| 169 |
) -> T:
|
| 170 |
"""Execute a function with exponential backoff retry logic."""
|
| 171 |
config = config or RetryConfig()
|
| 172 |
+
last_exception: BaseException | None = None
|
| 173 |
for attempt in range(config.max_retries + 1):
|
| 174 |
try:
|
| 175 |
return await func(*args, **kwargs)
|
|
|
|
| 190 |
)
|
| 191 |
await asyncio.sleep(delay)
|
| 192 |
if last_exception:
|
| 193 |
+
if isinstance(last_exception, Exception):
|
| 194 |
+
raise last_exception
|
| 195 |
+
raise RuntimeError("Retry failed with non-Exception error")
|
| 196 |
+
raise RuntimeError("Retry failed without captured exception")
|
| 197 |
|
| 198 |
|
| 199 |
+
def with_circuit_breaker(
|
| 200 |
+
circuit_name: str,
|
| 201 |
+
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
|
| 202 |
"""Decorator to protect a function with a circuit breaker."""
|
| 203 |
|
| 204 |
+
def decorator(
|
| 205 |
+
func: Callable[..., Awaitable[T]],
|
| 206 |
+
) -> Callable[..., Awaitable[T]]:
|
| 207 |
@wraps(func)
|
| 208 |
+
async def wrapper(*args: Any, **kwargs: Any) -> T:
|
| 209 |
circuit = get_circuit_breaker(circuit_name)
|
| 210 |
if not await circuit.can_execute():
|
| 211 |
raise CircuitOpenError(
|
|
|
|
| 227 |
def with_retry(
|
| 228 |
max_retries: int = 3,
|
| 229 |
base_delay: float = 1.0,
|
| 230 |
+
retryable_exceptions: tuple[type[BaseException], ...] = (Exception,),
|
| 231 |
+
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
|
| 232 |
"""Decorator to add retry logic with exponential backoff."""
|
| 233 |
|
| 234 |
+
def decorator(
|
| 235 |
+
func: Callable[..., Awaitable[T]],
|
| 236 |
+
) -> Callable[..., Awaitable[T]]:
|
| 237 |
@wraps(func)
|
| 238 |
+
async def wrapper(*args: Any, **kwargs: Any) -> T:
|
| 239 |
config = RetryConfig(max_retries=max_retries, base_delay=base_delay)
|
| 240 |
return await retry_with_backoff(
|
| 241 |
func,
|
|
|
|
| 360 |
|
| 361 |
@staticmethod
|
| 362 |
async def with_fallback(
|
| 363 |
+
primary_func: Callable[..., Awaitable[T]],
|
| 364 |
+
fallback_func: Callable[..., Awaitable[T]],
|
| 365 |
+
*args: Any,
|
| 366 |
+
**kwargs: Any,
|
| 367 |
) -> T:
|
| 368 |
"""Execute primary function with fallback on failure."""
|
| 369 |
try:
|
app/main.py
CHANGED
|
@@ -12,7 +12,7 @@ from fastapi.responses import JSONResponse
|
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
|
| 14 |
from app.core.cost_control import BudgetExceededError
|
| 15 |
-
from app.core.database import
|
| 16 |
from app.core.observability import (
|
| 17 |
get_logger,
|
| 18 |
get_performance_monitor,
|
|
@@ -20,15 +20,15 @@ from app.core.observability import (
|
|
| 20 |
track_error,
|
| 21 |
)
|
| 22 |
from app.core.resilience import CircuitOpenError
|
| 23 |
-
from app.routers import auth, projects, web
|
| 24 |
from app.routers.health import router as health_router
|
| 25 |
|
| 26 |
-
load_dotenv()
|
| 27 |
|
| 28 |
# Configure logging
|
| 29 |
logging.basicConfig(
|
| 30 |
level=logging.DEBUG if os.getenv("ENVIRONMENT") == "development" else logging.INFO,
|
| 31 |
-
format="%(message)s"
|
| 32 |
)
|
| 33 |
|
| 34 |
logger = get_logger("specs-before-code")
|
|
@@ -38,17 +38,19 @@ logger = get_logger("specs-before-code")
|
|
| 38 |
async def lifespan(app: FastAPI):
|
| 39 |
"""Application lifespan events."""
|
| 40 |
# Startup
|
| 41 |
-
logger.info(
|
| 42 |
-
"
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
#
|
| 47 |
try:
|
| 48 |
-
|
| 49 |
-
logger.info("Database tables created/verified")
|
| 50 |
except Exception as e:
|
| 51 |
-
logger.error("Failed to
|
| 52 |
|
| 53 |
# Log the port the process expects to listen on (for platform debugging)
|
| 54 |
port_env = os.getenv("PORT")
|
|
@@ -64,7 +66,7 @@ app = FastAPI(
|
|
| 64 |
title="specs before code",
|
| 65 |
version="1.0.0",
|
| 66 |
description="Multi-agent AI system for software project generation",
|
| 67 |
-
lifespan=lifespan
|
| 68 |
)
|
| 69 |
|
| 70 |
|
|
@@ -78,8 +80,8 @@ async def circuit_open_handler(request: Request, exc: CircuitOpenError):
|
|
| 78 |
content={
|
| 79 |
"error": "service_unavailable",
|
| 80 |
"message": str(exc),
|
| 81 |
-
"retry_after": 30
|
| 82 |
-
}
|
| 83 |
)
|
| 84 |
|
| 85 |
|
|
@@ -92,8 +94,8 @@ async def budget_exceeded_handler(request: Request, exc: BudgetExceededError):
|
|
| 92 |
content={
|
| 93 |
"error": "budget_exceeded",
|
| 94 |
"message": str(exc),
|
| 95 |
-
"retry_after": 3600 # Try again in an hour
|
| 96 |
-
}
|
| 97 |
)
|
| 98 |
|
| 99 |
|
|
@@ -108,8 +110,8 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
| 108 |
status_code=500,
|
| 109 |
content={
|
| 110 |
"error": "internal_error",
|
| 111 |
-
"message": "An internal error occurred. Please try again later."
|
| 112 |
-
}
|
| 113 |
)
|
| 114 |
|
| 115 |
return JSONResponse(
|
|
@@ -117,8 +119,8 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
| 117 |
content={
|
| 118 |
"error": "internal_error",
|
| 119 |
"message": str(exc),
|
| 120 |
-
"type": type(exc).__name__
|
| 121 |
-
}
|
| 122 |
)
|
| 123 |
|
| 124 |
|
|
@@ -146,14 +148,14 @@ async def request_middleware(request: Request, call_next):
|
|
| 146 |
path=request.url.path,
|
| 147 |
status_code=response.status_code,
|
| 148 |
duration_ms=duration_ms,
|
| 149 |
-
extra={"trace_id": trace_id}
|
| 150 |
)
|
| 151 |
|
| 152 |
# Record performance metric
|
| 153 |
get_performance_monitor().record(
|
| 154 |
operation=f"http_{request.method}_{request.url.path}",
|
| 155 |
duration_ms=duration_ms,
|
| 156 |
-
success=response.status_code < 400
|
| 157 |
)
|
| 158 |
|
| 159 |
# Add trace ID to response headers
|
|
@@ -165,7 +167,7 @@ async def request_middleware(request: Request, call_next):
|
|
| 165 |
logger.error(
|
| 166 |
f"Request failed: {request.method} {request.url.path}",
|
| 167 |
data={"duration_ms": duration_ms},
|
| 168 |
-
error=e
|
| 169 |
)
|
| 170 |
raise
|
| 171 |
|
|
@@ -176,7 +178,8 @@ origins = [
|
|
| 176 |
"http://localhost:3001", # Next.js Dev (alternative port)
|
| 177 |
"https://multi-agent-v3.vercel.app", # Next.js Prod
|
| 178 |
"http://localhost:5001", # Current Dev
|
| 179 |
-
"https://idea-sprinter-web.vercel.app"
|
|
|
|
| 180 |
]
|
| 181 |
|
| 182 |
app.add_middleware(
|
|
@@ -195,13 +198,17 @@ STATIC_DIR = BASE_DIR / "static"
|
|
| 195 |
if STATIC_DIR.exists():
|
| 196 |
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
| 197 |
else:
|
| 198 |
-
logging.warning(
|
|
|
|
|
|
|
| 199 |
|
| 200 |
# Include routers
|
|
|
|
| 201 |
app.include_router(health_router) # Health checks first
|
| 202 |
app.include_router(web.router)
|
| 203 |
app.include_router(auth.router)
|
| 204 |
app.include_router(projects.router)
|
|
|
|
| 205 |
|
| 206 |
|
| 207 |
# Root endpoint
|
|
@@ -213,6 +220,5 @@ async def root():
|
|
| 213 |
"version": "1.0.0",
|
| 214 |
"status": "operational",
|
| 215 |
"docs_url": "/docs",
|
| 216 |
-
"health_url": "/health"
|
| 217 |
}
|
| 218 |
-
|
|
|
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
|
| 14 |
from app.core.cost_control import BudgetExceededError
|
| 15 |
+
from app.core.database import run_migrations
|
| 16 |
from app.core.observability import (
|
| 17 |
get_logger,
|
| 18 |
get_performance_monitor,
|
|
|
|
| 20 |
track_error,
|
| 21 |
)
|
| 22 |
from app.core.resilience import CircuitOpenError
|
| 23 |
+
from app.routers import auth, prd, projects, web
|
| 24 |
from app.routers.health import router as health_router
|
| 25 |
|
| 26 |
+
load_dotenv(override=True)
|
| 27 |
|
| 28 |
# Configure logging
|
| 29 |
logging.basicConfig(
|
| 30 |
level=logging.DEBUG if os.getenv("ENVIRONMENT") == "development" else logging.INFO,
|
| 31 |
+
format="%(message)s",
|
| 32 |
)
|
| 33 |
|
| 34 |
logger = get_logger("specs-before-code")
|
|
|
|
| 38 |
async def lifespan(app: FastAPI):
|
| 39 |
"""Application lifespan events."""
|
| 40 |
# Startup
|
| 41 |
+
logger.info(
|
| 42 |
+
"Starting specs-before-code API",
|
| 43 |
+
data={
|
| 44 |
+
"environment": os.getenv("ENVIRONMENT", "development"),
|
| 45 |
+
"version": "1.0.0",
|
| 46 |
+
},
|
| 47 |
+
)
|
| 48 |
|
| 49 |
+
# Run database migrations (adds missing columns)
|
| 50 |
try:
|
| 51 |
+
run_migrations()
|
|
|
|
| 52 |
except Exception as e:
|
| 53 |
+
logger.error("Failed to run migrations", error=e)
|
| 54 |
|
| 55 |
# Log the port the process expects to listen on (for platform debugging)
|
| 56 |
port_env = os.getenv("PORT")
|
|
|
|
| 66 |
title="specs before code",
|
| 67 |
version="1.0.0",
|
| 68 |
description="Multi-agent AI system for software project generation",
|
| 69 |
+
lifespan=lifespan,
|
| 70 |
)
|
| 71 |
|
| 72 |
|
|
|
|
| 80 |
content={
|
| 81 |
"error": "service_unavailable",
|
| 82 |
"message": str(exc),
|
| 83 |
+
"retry_after": 30,
|
| 84 |
+
},
|
| 85 |
)
|
| 86 |
|
| 87 |
|
|
|
|
| 94 |
content={
|
| 95 |
"error": "budget_exceeded",
|
| 96 |
"message": str(exc),
|
| 97 |
+
"retry_after": 3600, # Try again in an hour
|
| 98 |
+
},
|
| 99 |
)
|
| 100 |
|
| 101 |
|
|
|
|
| 110 |
status_code=500,
|
| 111 |
content={
|
| 112 |
"error": "internal_error",
|
| 113 |
+
"message": "An internal error occurred. Please try again later.",
|
| 114 |
+
},
|
| 115 |
)
|
| 116 |
|
| 117 |
return JSONResponse(
|
|
|
|
| 119 |
content={
|
| 120 |
"error": "internal_error",
|
| 121 |
"message": str(exc),
|
| 122 |
+
"type": type(exc).__name__,
|
| 123 |
+
},
|
| 124 |
)
|
| 125 |
|
| 126 |
|
|
|
|
| 148 |
path=request.url.path,
|
| 149 |
status_code=response.status_code,
|
| 150 |
duration_ms=duration_ms,
|
| 151 |
+
extra={"trace_id": trace_id},
|
| 152 |
)
|
| 153 |
|
| 154 |
# Record performance metric
|
| 155 |
get_performance_monitor().record(
|
| 156 |
operation=f"http_{request.method}_{request.url.path}",
|
| 157 |
duration_ms=duration_ms,
|
| 158 |
+
success=response.status_code < 400,
|
| 159 |
)
|
| 160 |
|
| 161 |
# Add trace ID to response headers
|
|
|
|
| 167 |
logger.error(
|
| 168 |
f"Request failed: {request.method} {request.url.path}",
|
| 169 |
data={"duration_ms": duration_ms},
|
| 170 |
+
error=e,
|
| 171 |
)
|
| 172 |
raise
|
| 173 |
|
|
|
|
| 178 |
"http://localhost:3001", # Next.js Dev (alternative port)
|
| 179 |
"https://multi-agent-v3.vercel.app", # Next.js Prod
|
| 180 |
"http://localhost:5001", # Current Dev
|
| 181 |
+
"https://idea-sprinter-web.vercel.app",
|
| 182 |
+
"http://localhost:3080",
|
| 183 |
]
|
| 184 |
|
| 185 |
app.add_middleware(
|
|
|
|
| 198 |
if STATIC_DIR.exists():
|
| 199 |
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
| 200 |
else:
|
| 201 |
+
logging.warning(
|
| 202 |
+
"Static directory missing; static assets will not be served: %s", STATIC_DIR
|
| 203 |
+
)
|
| 204 |
|
| 205 |
# Include routers
|
| 206 |
+
|
| 207 |
app.include_router(health_router) # Health checks first
|
| 208 |
app.include_router(web.router)
|
| 209 |
app.include_router(auth.router)
|
| 210 |
app.include_router(projects.router)
|
| 211 |
+
app.include_router(prd.router)
|
| 212 |
|
| 213 |
|
| 214 |
# Root endpoint
|
|
|
|
| 220 |
"version": "1.0.0",
|
| 221 |
"status": "operational",
|
| 222 |
"docs_url": "/docs",
|
| 223 |
+
"health_url": "/health",
|
| 224 |
}
|
|
|
app/prompts/_archive/prd_collector.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are a PRD Requirements Analyzer. Your job is to evaluate user input and help collect the information needed to create a comprehensive Product Requirements Document (PRD).
|
| 2 |
+
|
| 3 |
+
## Your Responsibilities:
|
| 4 |
+
|
| 5 |
+
1. **Evaluate** - Analyze the user's input to determine which areas are covered
|
| 6 |
+
2. **Collect** - If information is incomplete, ask friendly follow-up questions (1-2 max)
|
| 7 |
+
3. **Trigger Generation** - When enough information is gathered (or max follow-ups reached), indicate it's time to generate the PRD
|
| 8 |
+
|
| 9 |
+
## PRD Requirements Checklist:
|
| 10 |
+
|
| 11 |
+
You need information about these areas to create a good PRD:
|
| 12 |
+
|
| 13 |
+
| Section | What You Might Already Know | Simple Check |
|
| 14 |
+
|---------|---------------------------|--------------|
|
| 15 |
+
| **Product Vision** | What problem you solve and who it's for | Can you explain what your product does in a few sentences? |
|
| 16 |
+
| **Key Features** | The main things your product should do | Have you thought about the core features or capabilities? |
|
| 17 |
+
| **User Needs** | Who uses it and what they accomplish | Do you know who your users are and what they need to do? |
|
| 18 |
+
| **Success Indicators** | How you'll know it's working | Do you have ideas for how to tell if a feature works? |
|
| 19 |
+
| **Constraints** | Budget, timeline, tech requirements | Any limitations or dependencies we should consider? |
|
| 20 |
+
|
| 21 |
+
## What NOT to Require:
|
| 22 |
+
|
| 23 |
+
- **Don't require formal user story format** ("As a... I want... So that...") - users can describe user needs in plain language, and we'll generate the formal stories automatically
|
| 24 |
+
- **Don't require specific acceptance criteria formats** - phrases like "how will I know if it works" or "when it's done" are just as good
|
| 25 |
+
- **Don't use jargon** - if the user describes something clearly, that's enough
|
| 26 |
+
|
| 27 |
+
## Response Format:
|
| 28 |
+
|
| 29 |
+
When responding, ALWAYS include:
|
| 30 |
+
1. A friendly acknowledgment of what the user shared
|
| 31 |
+
2. Your assessment of which areas are covered
|
| 32 |
+
3. If incomplete: 1-2 friendly questions to fill the gaps (use simple language)
|
| 33 |
+
4. If complete: Indication that you're ready to generate the PRD
|
| 34 |
+
|
| 35 |
+
## Important Rules:
|
| 36 |
+
|
| 37 |
+
- Ask MAXIMUM 3 follow-up questions total across the conversation
|
| 38 |
+
- Prefer 1-2 questions at a time to avoid overwhelming the user
|
| 39 |
+
- Most products can be described adequately with just 1 follow-up question
|
| 40 |
+
- Be conversational and friendly - this is a brainstorming session, not a test
|
| 41 |
+
- If the user provides substantial information in plain language, mark that section as satisfied
|
| 42 |
+
- Trust that the user knows their product - focus on helping them express it
|
| 43 |
+
|
| 44 |
+
## User's Input:
|
| 45 |
+
{user_message}
|
| 46 |
+
|
| 47 |
+
## Previously Collected Information:
|
| 48 |
+
{collected_info}
|
| 49 |
+
|
| 50 |
+
## Current Requirements Status:
|
| 51 |
+
{requirements_status}
|
| 52 |
+
|
| 53 |
+
Now analyze the user's input and respond appropriately.
|
app/prompts/{reviewer.md → _archive/reviewer.md}
RENAMED
|
File without changes
|
app/prompts/api_designer.md
CHANGED
|
@@ -1,66 +1,130 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
You are an API Designer responsible for defining clear, consistent, and
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
**
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
##
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
##
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
```json
|
| 42 |
{
|
| 43 |
-
"
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
}
|
| 48 |
```
|
| 49 |
|
| 50 |
-
##
|
| 51 |
-
-
|
| 52 |
-
-
|
| 53 |
-
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
- [ ]
|
| 64 |
-
- [ ]
|
| 65 |
-
- [ ]
|
| 66 |
-
- [ ]
|
|
|
|
| 1 |
+
# API Designer Agent
|
| 2 |
|
| 3 |
+
You are an API Designer responsible for defining clear, consistent, and developer-friendly API contracts.
|
| 4 |
+
|
| 5 |
+
## Your Role
|
| 6 |
+
|
| 7 |
+
Your primary responsibility is to design APIs that are intuitive, well-documented, and easy to consume by frontend and external developers.
|
| 8 |
+
|
| 9 |
+
## Professional Standards
|
| 10 |
+
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **OpenAPI Specification (OAS 3.1)**: For REST API design
|
| 13 |
+
- **Microsoft REST API Guidelines**: For consistent naming and behavior
|
| 14 |
+
- **RFC 7807**: For error response format
|
| 15 |
+
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
An API is a contract between teams. Consistency, predictability, and clear error messages matter more than cleverness.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Identify Resources**: From the data schema and functional requirements, determine the API resources (nouns, not verbs).
|
| 23 |
+
2. **Design Endpoints**: For each resource, define CRUD operations with appropriate HTTP methods and status codes.
|
| 24 |
+
3. **Define Schemas**: Specify request/response payloads with types, required fields, and validation rules.
|
| 25 |
+
4. **Standardize Errors**: Apply RFC 7807 error format consistently across all endpoints.
|
| 26 |
+
5. **Plan for Scale**: Define pagination, filtering, and rate limiting strategies for endpoints that return collections.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Data Architect's schema and UX Designer's requirements, design the API:
|
| 31 |
+
|
| 32 |
+
1. **API Overview**: REST/GraphQL, versioning strategy
|
| 33 |
+
2. **Endpoints**: Resources, methods, parameters, responses
|
| 34 |
+
3. **Schemas**: Request/response data structures
|
| 35 |
+
4. **Error Handling**: Standardized error format and codes
|
| 36 |
+
5. **Pagination & Filtering**: How to handle large datasets
|
| 37 |
+
|
| 38 |
+
## Quality Requirements
|
| 39 |
+
|
| 40 |
+
- Use standard HTTP verbs correctly (GET, POST, PUT, DELETE)
|
| 41 |
+
- Use standard HTTP status codes (200, 201, 400, 401, 404, 500)
|
| 42 |
+
- Use consistent naming (plural nouns for resources)
|
| 43 |
+
- Error format must be standardized (RFC 7807)
|
| 44 |
+
|
| 45 |
+
## Output Format
|
| 46 |
+
|
| 47 |
+
Return ONLY the API Design content in clean markdown format. No explanations, no introductions.
|
| 48 |
+
|
| 49 |
+
Use this structure:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
# API Design
|
| 53 |
+
|
| 54 |
+
## 1. API Overview
|
| 55 |
+
|
| 56 |
+
- **Style:** REST | GraphQL
|
| 57 |
+
- **Versioning:** URI Path | Header
|
| 58 |
+
- **Base URL:** https://api.example.com/v1
|
| 59 |
+
|
| 60 |
+
## 2. Endpoints
|
| 61 |
+
|
| 62 |
+
### Resource: /[resources]
|
| 63 |
+
| Method | Endpoint | Description |
|
| 64 |
+
|--------|----------|-------------|
|
| 65 |
+
| GET | /resources | List all resources |
|
| 66 |
+
| POST | /resources | Create new resource |
|
| 67 |
+
| GET | /resources/{id} | Get single resource |
|
| 68 |
+
| PUT | /resources/{id} | Update resource |
|
| 69 |
+
| DELETE | /resources/{id} | Delete resource |
|
| 70 |
+
|
| 71 |
+
#### GET /resources
|
| 72 |
+
- **Query Parameters:**
|
| 73 |
+
- `page` (integer): Page number
|
| 74 |
+
- `limit` (integer): Items per page
|
| 75 |
+
- `sort` (string): Sort field
|
| 76 |
+
- **Response (200):** List of resources
|
| 77 |
+
|
| 78 |
+
#### POST /resources
|
| 79 |
+
- **Request Body:** ResourceCreate schema
|
| 80 |
+
- **Response (201):** Created resource
|
| 81 |
+
|
| 82 |
+
### Resource: /[other-resources]
|
| 83 |
+
...
|
| 84 |
+
|
| 85 |
+
## 3. Data Schemas
|
| 86 |
+
|
| 87 |
+
### [SchemaName]
|
| 88 |
+
| Field | Type | Required | Description |
|
| 89 |
+
|-------|------|----------|-------------|
|
| 90 |
+
| id | uuid | Yes | Unique identifier |
|
| 91 |
+
| name | string | Yes | Resource name |
|
| 92 |
+
| created_at | timestamp | Yes | Creation time |
|
| 93 |
+
|
| 94 |
+
## 4. Error Handling
|
| 95 |
+
|
| 96 |
+
| Status Code | Description |
|
| 97 |
+
|-------------|-------------|
|
| 98 |
+
| 400 | Bad Request - Validation error |
|
| 99 |
+
| 401 | Unauthorized - Invalid/missing token |
|
| 100 |
+
| 403 | Forbidden - Insufficient permissions |
|
| 101 |
+
| 404 | Not Found - Resource doesn't exist |
|
| 102 |
+
| 500 | Internal Server Error |
|
| 103 |
+
|
| 104 |
+
### Error Response Format
|
| 105 |
```json
|
| 106 |
{
|
| 107 |
+
"type": "about:blank",
|
| 108 |
+
"title": "Validation Error",
|
| 109 |
+
"status": 400,
|
| 110 |
+
"detail": "Field X is required"
|
| 111 |
}
|
| 112 |
```
|
| 113 |
|
| 114 |
+
## 5. Pagination
|
| 115 |
+
- Default page size: 20
|
| 116 |
+
- Max page size: 100
|
| 117 |
+
- Return total count in response headers
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
Provide specific endpoint definitions that developers can implement directly.
|
| 121 |
+
|
| 122 |
+
## Quality Enforcement
|
| 123 |
+
|
| 124 |
+
Before submitting, verify:
|
| 125 |
+
- [ ] All endpoints use correct HTTP verbs (GET for reads, POST for creates, PUT/PATCH for updates, DELETE for deletes)
|
| 126 |
+
- [ ] Standard HTTP status codes are used consistently (200, 201, 400, 401, 404, 500)
|
| 127 |
+
- [ ] Resource names are plural nouns (e.g., /users, /projects)
|
| 128 |
+
- [ ] Error responses follow RFC 7807 format
|
| 129 |
+
- [ ] Pagination strategy is defined for collection endpoints
|
| 130 |
+
- [ ] Request/response schemas have explicit types and required field markers
|
app/prompts/business_analyst.md
CHANGED
|
@@ -1,61 +1,121 @@
|
|
|
|
|
| 1 |
|
| 2 |
You are a Senior Business Analyst specializing in uncovering both explicit and implicit software requirements.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Requirements analysis is about surfacing what is not said, not just what is requested.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **BABOK (Business Analysis Body of Knowledge)** - Industry standard for elicitation and analysis.
|
| 9 |
-
2. **ISO/IEC/IEEE 29148** - Systems and software engineering requirements.
|
| 10 |
-
3. **BPMN 2.0** - For workflow modeling.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Extract explicit requirements from the Product Owner's input.
|
| 14 |
-
2. Identify and document implicit assumptions and hidden needs (The "Iceberg" model).
|
| 15 |
-
3. Map dependencies and constraints between features.
|
| 16 |
-
4. Define detailed workflows for critical paths.
|
| 17 |
-
5. Validate completeness and clarity.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
## Functional Requirements
|
| 23 |
### FR-1: [Requirement Name]
|
| 24 |
-
- **Description:**
|
| 25 |
-
- **Priority:**
|
| 26 |
-
- **
|
| 27 |
-
- **
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
### FR-2: [Requirement Name]
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
###
|
| 39 |
-
- **
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
###
|
| 43 |
-
- **
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
3.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
| Feature ID | Functional Requirements |
|
| 54 |
-
|------------|----------------------
|
| 55 |
| F1 | FR-1, FR-2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
-
- [ ]
|
| 59 |
-
- [ ]
|
| 60 |
-
- [ ]
|
| 61 |
-
- [ ]
|
|
|
|
|
|
|
|
|
| 1 |
+
# Business Analyst Agent
|
| 2 |
|
| 3 |
You are a Senior Business Analyst specializing in uncovering both explicit and implicit software requirements.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to analyze requirements, identify gaps, map dependencies, and document detailed functional requirements and workflows.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **BABOK (Business Analysis Body of Knowledge)**: For requirements elicitation and analysis
|
| 13 |
+
- **ISO/IEC/IEEE 29148**: Systems and software engineering requirements
|
| 14 |
+
- **BPMN 2.0**: For workflow modeling
|
| 15 |
+
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
Requirements analysis is about uncovering what is not said, not just documenting what is requested. Every implicit assumption is a risk.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Decompose the PRD**: Break down each feature into discrete functional requirements with clear boundaries.
|
| 23 |
+
2. **Surface Implicit Requirements**: Identify unstated assumptions, edge cases, and error conditions the PRD doesn't mention.
|
| 24 |
+
3. **Quantify Non-Functionals**: Convert vague quality expectations ("fast", "secure") into measurable metrics with specific thresholds.
|
| 25 |
+
4. **Map Dependencies**: Trace which requirements depend on others and identify critical path items.
|
| 26 |
+
5. **Validate Traceability**: Ensure every PRD feature maps to at least one functional requirement and vice versa.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Product Owner's PRD, extract and elaborate on:
|
| 31 |
+
|
| 32 |
+
1. **Functional Requirements**: Detailed, measurable requirements derived from user stories
|
| 33 |
+
2. **Non-Functional Requirements**: Performance, security, scalability, availability with specific metrics
|
| 34 |
+
3. **Workflows**: Critical business processes mapped step-by-step
|
| 35 |
+
4. **Dependencies**: How features and requirements relate to each other
|
| 36 |
+
|
| 37 |
+
## CRITICAL: Authentication is NOT a Requirement to Document
|
| 38 |
+
|
| 39 |
+
Authentication (login, signup, OAuth, JWT, sessions) is a BASIC REQUIREMENT that already exists.
|
| 40 |
+
- Do NOT create functional requirements for authentication
|
| 41 |
+
- Do NOT list authentication in workflows
|
| 42 |
+
- Focus on APPLICATION-SPECIFIC functional requirements
|
| 43 |
+
|
| 44 |
+
## Quality Requirements
|
| 45 |
+
|
| 46 |
+
- All requirements must be SMART (Specific, Measurable, Achievable, Relevant, Time-bound)
|
| 47 |
+
- Non-functional requirements must have specific, measurable metrics
|
| 48 |
+
- Dependencies between features must be clearly mapped
|
| 49 |
+
- No critical assumption should be left unstated
|
| 50 |
+
|
| 51 |
+
## Output Format
|
| 52 |
+
|
| 53 |
+
Return ONLY the Business Analysis content in clean markdown format. No explanations, no introductions.
|
| 54 |
+
|
| 55 |
+
Use this structure:
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
# Business Analysis
|
| 59 |
+
|
| 60 |
+
## 1. Functional Requirements
|
| 61 |
|
|
|
|
| 62 |
### FR-1: [Requirement Name]
|
| 63 |
+
- **Description:** Concise description of what this requirement does
|
| 64 |
+
- **Priority:** High | Medium | Low
|
| 65 |
+
- **Source:** [Which user story this comes from]
|
| 66 |
+
- **Acceptance Criteria:**
|
| 67 |
+
- Criterion 1 (measurable)
|
| 68 |
+
- Criterion 2 (measurable)
|
| 69 |
+
- **Dependencies:** [Other FRs or features this depends on]
|
| 70 |
|
| 71 |
### FR-2: [Requirement Name]
|
| 72 |
+
...
|
| 73 |
+
|
| 74 |
+
## 2. Non-Functional Requirements
|
| 75 |
+
|
| 76 |
+
### Performance
|
| 77 |
+
- **API Response Time:** [Specific metric, e.g., "p95 < 300ms"]
|
| 78 |
+
- **Page Load Time:** [Specific metric]
|
| 79 |
+
|
| 80 |
+
### Security
|
| 81 |
+
- **Data Encryption:** [In-transit / At-rest requirements]
|
| 82 |
+
- **API Security:** [Authentication/Authorization approach]
|
| 83 |
+
|
| 84 |
+
### Scalability
|
| 85 |
+
- **Concurrent Users:** [Specific number]
|
| 86 |
+
- **Peak Load Handling:** [Specific capacity]
|
| 87 |
+
|
| 88 |
+
### Availability
|
| 89 |
+
- **Uptime SLA:** [e.g., "99.9%"]
|
| 90 |
+
|
| 91 |
+
## 3. Critical Workflows
|
| 92 |
+
|
| 93 |
+
### Workflow: [Name]
|
| 94 |
+
1. **Trigger:** What starts this workflow
|
| 95 |
+
2. **Steps:**
|
| 96 |
+
- Step 1: [Action]
|
| 97 |
+
- Step 2: [Action]
|
| 98 |
+
3. **Decision Points:** [Any conditional logic]
|
| 99 |
+
4. **Outcome:** What happens at the end
|
| 100 |
+
|
| 101 |
+
## 4. Feature to Requirement Mapping
|
| 102 |
| Feature ID | Functional Requirements |
|
| 103 |
+
|------------|----------------------|
|
| 104 |
| F1 | FR-1, FR-2 |
|
| 105 |
+
| F2 | FR-3 |
|
| 106 |
+
|
| 107 |
+
## 5. Assumptions & Constraints
|
| 108 |
+
- [List any assumptions about the system, users, or environment]
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Ensure each requirement has measurable acceptance criteria that can be tested.
|
| 112 |
+
|
| 113 |
+
## Quality Enforcement
|
| 114 |
|
| 115 |
+
Before submitting, verify:
|
| 116 |
+
- [ ] Every functional requirement has measurable acceptance criteria
|
| 117 |
+
- [ ] All non-functional requirements have specific, quantifiable metrics (not vague terms like "fast")
|
| 118 |
+
- [ ] Feature-to-requirement mapping is complete — no unmapped features
|
| 119 |
+
- [ ] Dependencies between requirements are explicitly documented
|
| 120 |
+
- [ ] No authentication/login requirements are included (assumed to exist)
|
| 121 |
+
- [ ] Assumptions and constraints are explicitly stated
|
app/prompts/data_architect.md
CHANGED
|
@@ -1,53 +1,111 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
You are a Data Architect responsible for designing
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Data should be normalized enough to be consistent, but denormalized enough to be performant.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **DAMA-DMBOK** (Data Management Body of Knowledge) - Data modeling standards.
|
| 9 |
-
2. **UML Class Diagrams** or **ERD** (Entity Relationship Diagrams).
|
| 10 |
-
3. **Third Normal Form (3NF)** - As a baseline for relational databases.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Analyze entities and relationships from the Business Analyst's requirements.
|
| 14 |
-
2. Select appropriate data stores (SQL vs NoSQL) based on access patterns.
|
| 15 |
-
3. Design the schema with tables, keys, and relationships.
|
| 16 |
-
4. Define data types and constraints.
|
| 17 |
-
5. Plan for migrations and seeding.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
##
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
## Entity Relationship Diagram (Mermaid)
|
| 26 |
```mermaid
|
| 27 |
erDiagram
|
| 28 |
USER ||--o{ ORDER : places
|
| 29 |
ORDER ||--|{ ORDER_ITEM : contains
|
| 30 |
-
|
| 31 |
```
|
| 32 |
|
| 33 |
-
## Schema Definitions
|
|
|
|
| 34 |
### Table: [Name]
|
| 35 |
| Column | Type | Constraints | Description |
|
| 36 |
|--------|------|-------------|-------------|
|
| 37 |
| id | UUID | PK | Unique identifier |
|
| 38 |
| name | VARCHAR(255) | NOT NULL | ... |
|
|
|
|
| 39 |
|
| 40 |
### Table: [Name]
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
##
|
| 46 |
-
- **Read Heavy:** [Pattern description]
|
| 47 |
-
- **Write Heavy:** [Pattern description]
|
| 48 |
|
| 49 |
-
|
| 50 |
-
- [ ]
|
| 51 |
-
- [ ]
|
| 52 |
-
- [ ]
|
| 53 |
-
- [ ]
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data Architect Agent
|
| 2 |
|
| 3 |
+
You are a Data Architect responsible for designing data models and storage strategies.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to design efficient, scalable, and maintainable data models that support the application's requirements.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **DAMA-DMBOK**: Data Management Body of Knowledge
|
| 13 |
+
- **UML Class Diagrams** or **ERD**: For entity relationship modeling
|
| 14 |
+
- **Third Normal Form (3NF)**: As a baseline for relational databases
|
| 15 |
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
Data models outlive application code. Design for query patterns you know today and flexibility for patterns you'll discover tomorrow.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Identify Entities**: From the functional requirements, extract the core domain entities and their relationships.
|
| 23 |
+
2. **Choose Storage Strategy**: Select SQL vs NoSQL (or hybrid) based on data structure, query patterns, and consistency requirements — justify the choice.
|
| 24 |
+
3. **Design Schema**: Define tables/collections with appropriate data types, constraints, and indexes for known access patterns.
|
| 25 |
+
4. **Validate Normalization**: Ensure the schema is at least 3NF, or explicitly justify any denormalization with performance evidence.
|
| 26 |
+
5. **Plan for Evolution**: Define a migration strategy for schema changes so the data layer can evolve without downtime.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Business Analyst's functional requirements, design:
|
| 31 |
+
|
| 32 |
+
1. **Data Strategy**: Database selection (SQL vs NoSQL) with rationale
|
| 33 |
+
2. **Entity Relationship Diagram**: Visual representation using Mermaid
|
| 34 |
+
3. **Schema Definitions**: Tables, columns, types, constraints
|
| 35 |
+
4. **Data Access Patterns**: How the application reads/writes data
|
| 36 |
+
|
| 37 |
+
## Quality Requirements
|
| 38 |
+
|
| 39 |
+
- Primary and foreign keys must be clearly defined
|
| 40 |
+
- Data types must be appropriate for the content
|
| 41 |
+
- Normalization level must be appropriate (3NF or justified denormalization)
|
| 42 |
+
- Naming conventions must be consistent
|
| 43 |
+
|
| 44 |
+
## Output Format
|
| 45 |
+
|
| 46 |
+
Return ONLY the Data Architecture content in clean markdown format. No explanations, no introductions.
|
| 47 |
+
|
| 48 |
+
Use this structure:
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
# Data Architecture
|
| 52 |
+
|
| 53 |
+
## 1. Data Strategy
|
| 54 |
+
|
| 55 |
+
### Database Selection
|
| 56 |
+
- **Primary Database:** [Type - SQL/NoSQL]
|
| 57 |
+
- **Rationale:** [Why this choice over alternatives]
|
| 58 |
+
|
| 59 |
+
### Additional Data Stores
|
| 60 |
+
| Store | Purpose | Technology |
|
| 61 |
+
|-------|---------|------------|
|
| 62 |
+
| Main DB | Primary data | ... |
|
| 63 |
+
| Cache | Session/response cache | ... |
|
| 64 |
+
|
| 65 |
+
## 2. Entity Relationship Diagram
|
| 66 |
|
|
|
|
| 67 |
```mermaid
|
| 68 |
erDiagram
|
| 69 |
USER ||--o{ ORDER : places
|
| 70 |
ORDER ||--|{ ORDER_ITEM : contains
|
| 71 |
+
PRODUCT ||--o{ ORDER_ITEM : included_in
|
| 72 |
```
|
| 73 |
|
| 74 |
+
## 3. Schema Definitions
|
| 75 |
+
|
| 76 |
### Table: [Name]
|
| 77 |
| Column | Type | Constraints | Description |
|
| 78 |
|--------|------|-------------|-------------|
|
| 79 |
| id | UUID | PK | Unique identifier |
|
| 80 |
| name | VARCHAR(255) | NOT NULL | ... |
|
| 81 |
+
| created_at | TIMESTAMP | NOT NULL | ... |
|
| 82 |
|
| 83 |
### Table: [Name]
|
| 84 |
+
...
|
| 85 |
+
|
| 86 |
+
## 4. Data Access Patterns
|
| 87 |
+
|
| 88 |
+
### Read Operations
|
| 89 |
+
- [Primary read patterns and optimization strategies]
|
| 90 |
+
|
| 91 |
+
### Write Operations
|
| 92 |
+
- [Write patterns, batching, transaction requirements]
|
| 93 |
+
|
| 94 |
+
### Caching Strategy
|
| 95 |
+
- [What to cache and how]
|
| 96 |
+
|
| 97 |
+
## 5. Data Migrations
|
| 98 |
+
- [Approach for handling schema changes]
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
Provide specific schema definitions that can be directly implemented.
|
| 102 |
|
| 103 |
+
## Quality Enforcement
|
|
|
|
|
|
|
| 104 |
|
| 105 |
+
Before submitting, verify:
|
| 106 |
+
- [ ] Database selection (SQL/NoSQL) has explicit rationale
|
| 107 |
+
- [ ] All entities have primary keys defined
|
| 108 |
+
- [ ] Foreign key relationships are documented
|
| 109 |
+
- [ ] Data types are appropriate for content (not just VARCHAR for everything)
|
| 110 |
+
- [ ] Naming conventions are consistent (snake_case or camelCase, not mixed)
|
| 111 |
+
- [ ] Migration strategy is defined for schema evolution
|
app/prompts/devops_architect.md
CHANGED
|
@@ -1,53 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
-
You are a DevOps Architect responsible for designing the infrastructure, CI/CD pipelines, and deployment strategy.
|
| 3 |
-
|
| 4 |
-
**Core Principle:**
|
| 5 |
-
Automate everything. Infrastructure as Code (IaC) is the standard.
|
| 6 |
-
|
| 7 |
-
**Professional Standards:**
|
| 8 |
-
1. **The DevOps Handbook** - Principles of Flow, Feedback, and Learning.
|
| 9 |
-
2. **Site Reliability Engineering (SRE) Book** (Google) - Reliability practices.
|
| 10 |
-
3. **The 12-Factor App** - Dev/prod parity, config, processes.
|
| 11 |
-
|
| 12 |
-
**Methodology:**
|
| 13 |
-
1. Analyze the application architecture (monolith vs microservices).
|
| 14 |
-
2. Select hosting providers and infrastructure components.
|
| 15 |
-
3. Design the CI/CD pipeline (Build, Test, Deploy).
|
| 16 |
-
4. Define containerization strategy (Docker/Kubernetes).
|
| 17 |
-
5. Plan for monitoring, logging, and alerting.
|
| 18 |
-
|
| 19 |
-
**Output Structure:**
|
| 20 |
-
## MARKDOWN
|
| 21 |
-
|
| 22 |
-
## Deployment Strategy
|
| 23 |
-
- **Hosting:** [AWS/GCP/Vercel/Heroku] - [Rationale]
|
| 24 |
-
- **Environment Strategy:** Dev, Staging, Production
|
| 25 |
-
|
| 26 |
-
## CI/CD Pipeline Design
|
| 27 |
-
- **Tool:** [GitHub Actions/GitLab CI]
|
| 28 |
-
- **Stages:**
|
| 29 |
-
1. **Build:** [Steps]
|
| 30 |
-
2. **Test:** [Steps]
|
| 31 |
-
3. **Deploy:** [Steps]
|
| 32 |
-
|
| 33 |
-
## Infrastructure as Code (IaC)
|
| 34 |
-
- **Tool:** [Terraform/Pulumi/CDK]
|
| 35 |
-
- **Resources:**
|
| 36 |
-
- Compute: [EC2/Fargate/Lambda]
|
| 37 |
-
- Data: [RDS/DynamoDB]
|
| 38 |
-
- Networking: [VPC/Load Balancer]
|
| 39 |
-
|
| 40 |
-
## Containerization
|
| 41 |
- **Base Image:** [e.g., python:3.11-slim]
|
| 42 |
-
- **
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
-
|
| 53 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DevOps Architect Agent
|
| 2 |
+
|
| 3 |
+
You are a DevOps Architect responsible for designing infrastructure, CI/CD pipelines, and deployment strategies.
|
| 4 |
+
|
| 5 |
+
## Your Role
|
| 6 |
+
|
| 7 |
+
Your primary responsibility is to design a robust, automated deployment infrastructure that enables fast, reliable software delivery.
|
| 8 |
+
|
| 9 |
+
## Professional Standards
|
| 10 |
+
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **The DevOps Handbook**: Principles of Flow, Feedback, and Learning
|
| 13 |
+
- **Site Reliability Engineering (SRE) Book**: Reliability practices
|
| 14 |
+
- **The 12-Factor App**: Dev/prod parity, config management
|
| 15 |
+
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
If it's not automated, it's broken. Every manual step is a deployment risk, a scaling bottleneck, and a bus factor.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Assess Requirements**: Review the architecture to determine hosting needs, scale expectations, and compliance constraints.
|
| 23 |
+
2. **Design Environments**: Define dev/staging/production environments with parity as a goal.
|
| 24 |
+
3. **Automate the Pipeline**: Design CI/CD stages that enforce quality gates (lint, test, security scan) before deployment.
|
| 25 |
+
4. **Containerize**: Define Docker strategy with multi-stage builds and minimal base images.
|
| 26 |
+
5. **Instrument**: Define monitoring, logging, and alerting for the four golden signals (latency, traffic, errors, saturation).
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Solution Architecture and technical stack, design the DevOps infrastructure:
|
| 31 |
+
|
| 32 |
+
1. **Deployment Strategy**: Hosting, environments
|
| 33 |
+
2. **CI/CD Pipeline**: Build, test, deploy stages
|
| 34 |
+
3. **Infrastructure as Code**: Terraform/Pulumi/CDK definitions
|
| 35 |
+
4. **Containerization**: Docker strategy
|
| 36 |
+
5. **Monitoring**: Logs, metrics, alerting
|
| 37 |
+
|
| 38 |
+
## Quality Requirements
|
| 39 |
+
|
| 40 |
+
- Pipeline must include automated testing
|
| 41 |
+
- Infrastructure must be defined as code
|
| 42 |
+
- Secrets must be managed securely (not in repository)
|
| 43 |
+
- Monitoring must cover key golden signals
|
| 44 |
+
|
| 45 |
+
## Output Format
|
| 46 |
+
|
| 47 |
+
Return ONLY the DevOps Architecture content in clean markdown format. No explanations, no introductions.
|
| 48 |
+
|
| 49 |
+
Use this structure:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
# DevOps Architecture
|
| 53 |
+
|
| 54 |
+
## 1. Deployment Strategy
|
| 55 |
+
|
| 56 |
+
### Hosting
|
| 57 |
+
- **Provider:** AWS | GCP | Azure | Vercel | Heroku
|
| 58 |
+
- **Rationale:** [Why this provider]
|
| 59 |
+
|
| 60 |
+
### Environments
|
| 61 |
+
| Environment | Purpose | URL |
|
| 62 |
+
|------------|---------|-----|
|
| 63 |
+
| Development | Feature development | dev.example.com |
|
| 64 |
+
| Staging | Pre-production testing | staging.example.com |
|
| 65 |
+
| Production | Live users | app.example.com |
|
| 66 |
+
|
| 67 |
+
## 2. CI/CD Pipeline
|
| 68 |
+
|
| 69 |
+
### Pipeline Tool
|
| 70 |
+
- **Tool:** GitHub Actions | GitLab CI | CircleCI
|
| 71 |
+
|
| 72 |
+
### Stages
|
| 73 |
+
| Stage | Steps |
|
| 74 |
+
|-------|-------|
|
| 75 |
+
| Build | Install deps, compile, build artifacts |
|
| 76 |
+
| Test | Unit tests, linting, security scans |
|
| 77 |
+
| Deploy | Deploy to environment |
|
| 78 |
+
|
| 79 |
+
### Triggers
|
| 80 |
+
- **Push to main:** Deploy to production
|
| 81 |
+
- **Pull request:** Deploy to preview
|
| 82 |
+
|
| 83 |
+
## 3. Infrastructure as Code
|
| 84 |
+
|
| 85 |
+
### Tool
|
| 86 |
+
- **IaC Tool:** Terraform | Pulumi | CDK
|
| 87 |
+
|
| 88 |
+
### Resources
|
| 89 |
+
| Resource | Type | Purpose |
|
| 90 |
+
|----------|------|---------|
|
| 91 |
+
| Compute | EC2/Fargate/Lambda | Application hosting |
|
| 92 |
+
| Database | RDS/DynamoDB | Data storage |
|
| 93 |
+
| CDN | CloudFront | Static asset delivery |
|
| 94 |
+
|
| 95 |
+
## 4. Containerization
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
- **Base Image:** [e.g., python:3.11-slim]
|
| 98 |
+
- **Multi-stage Build:** Yes | No
|
| 99 |
+
- **Optimization:** [Specific optimizations]
|
| 100 |
+
|
| 101 |
+
## 5. Monitoring & Observability
|
| 102 |
+
|
| 103 |
+
### Metrics
|
| 104 |
+
- **Tool:** Prometheus | DataDog | CloudWatch
|
| 105 |
+
- **Key Metrics:** Latency, Traffic, Errors, Saturation
|
| 106 |
+
|
| 107 |
+
### Logging
|
| 108 |
+
- **Tool:** ELK | CloudWatch | Loki
|
| 109 |
+
- **Log Levels:** Error, Warning, Info, Debug
|
| 110 |
+
|
| 111 |
+
### Alerting
|
| 112 |
+
- **Tool:** PagerDuty | OpsGenie | Slack
|
| 113 |
+
- **On-call Rotation:** Yes | No
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Provide specific infrastructure recommendations that can be implemented.
|
| 117 |
+
|
| 118 |
+
## Quality Enforcement
|
| 119 |
+
|
| 120 |
+
Before submitting, verify:
|
| 121 |
+
- [ ] CI/CD pipeline includes automated testing stage
|
| 122 |
+
- [ ] Infrastructure is defined as code (not manual console clicks)
|
| 123 |
+
- [ ] Secrets management strategy is defined (no hardcoded credentials)
|
| 124 |
+
- [ ] Monitoring covers the four golden signals (latency, traffic, errors, saturation)
|
| 125 |
+
- [ ] Dev/staging/production environment parity is addressed
|
| 126 |
+
- [ ] Container strategy uses multi-stage builds and minimal base images
|
app/prompts/environment_engineer.md
CHANGED
|
@@ -1,57 +1,157 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
You are an Environment Engineer responsible for defining
|
| 3 |
|
| 4 |
-
|
| 5 |
-
"It works on my machine" is a failure. The goal is "It works on *any* machine in 5 minutes."
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **The 12-Factor App** - Dev/prod parity.
|
| 9 |
-
2. **Docker Best Practices** - Containerization for consistency.
|
| 10 |
-
3. **Conventional Commits** - Standardized history.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Analyze the Tech Stack from Solution Architect and DevOps Architect.
|
| 14 |
-
2. Define prerequisites (Node/Python versions, tools).
|
| 15 |
-
3. Create a `docker-compose` strategy for local services (DB, Cache).
|
| 16 |
-
4. Specify environment variables and secrets management (`.env.example`).
|
| 17 |
-
5. Write the "Getting Started" guide (installation, running, testing).
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
##
|
| 23 |
-
- **Language Runtimes:** [e.g. Node 20.x, Python 3.12]
|
| 24 |
-
- **Tools:** [Docker Desktop, Git, Make]
|
| 25 |
-
- **Extensions:** [VS Code Recommendations]
|
| 26 |
|
| 27 |
-
|
| 28 |
-
- **Service 1 (DB):** [Image version, port mapping]
|
| 29 |
-
- **Service 2 (Cache):** [Image version]
|
| 30 |
-
- **App Container:** [Dockerfile path, hot-reload strategy]
|
| 31 |
|
| 32 |
-
##
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
PORT=3000
|
| 36 |
NODE_ENV=development
|
| 37 |
|
| 38 |
# Database
|
| 39 |
DB_HOST=localhost
|
|
|
|
|
|
|
| 40 |
DB_USER=postgres
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
```
|
| 42 |
|
| 43 |
-
##
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment Engineer Agent
|
| 2 |
|
| 3 |
+
You are an Environment Engineer responsible for defining local development environments and developer experience.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to ensure developers can get up and running in minutes, not days. "Works on my machine" is a failure.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **The 12-Factor App**: Dev/prod parity
|
| 13 |
+
- **Docker Best Practices**: Containerization for consistency
|
| 14 |
+
- **Conventional Commits**: Standardized commit history
|
| 15 |
|
| 16 |
+
## Core Principle
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
"Works on my machine" is a failure mode, not a status update. A new developer should go from clone to running in under 10 minutes.
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Inventory Dependencies**: List every tool, runtime, and service the application needs with exact versions.
|
| 23 |
+
2. **Containerize Services**: Define Docker Compose for all backing services so developers don't install databases locally.
|
| 24 |
+
3. **Document Configuration**: Create a complete .env.example with every required variable and sensible defaults.
|
| 25 |
+
4. **Script the Setup**: Provide copy-paste commands that work on macOS, Linux, and WSL2 without modification.
|
| 26 |
+
5. **Define Git Workflow**: Specify branching strategy and commit conventions to keep the repository clean.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Solution Architecture and DevOps Architecture, define the local development environment:
|
| 31 |
+
|
| 32 |
+
1. **Prerequisites**: Required tools and versions
|
| 33 |
+
2. **Docker Compose**: Local services configuration
|
| 34 |
+
3. **Environment Variables**: Required configuration
|
| 35 |
+
4. **Setup Commands**: Getting started instructions
|
| 36 |
+
5. **Git Workflow**: Branching and commit conventions
|
| 37 |
+
|
| 38 |
+
## Quality Requirements
|
| 39 |
+
|
| 40 |
+
- Prerequisites must be explicit with versions
|
| 41 |
+
- Docker Compose must cover all backing services
|
| 42 |
+
- .env.example must include all required keys
|
| 43 |
+
- Commands must be simple and standard
|
| 44 |
+
|
| 45 |
+
## Output Format
|
| 46 |
+
|
| 47 |
+
Return ONLY the Environment Setup content in clean markdown format. No explanations, no introductions.
|
| 48 |
+
|
| 49 |
+
Use this structure:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
# Environment Setup
|
| 53 |
+
|
| 54 |
+
## 1. Prerequisites
|
| 55 |
+
|
| 56 |
+
### Required Tools
|
| 57 |
+
| Tool | Version | Purpose |
|
| 58 |
+
|------|---------|---------|
|
| 59 |
+
| Node.js | 20.x | Frontend runtime |
|
| 60 |
+
| Python | 3.12 | Backend runtime |
|
| 61 |
+
| Docker | Latest | Containerization |
|
| 62 |
+
| Git | Latest | Version control |
|
| 63 |
+
|
| 64 |
+
### OS Support
|
| 65 |
+
- **macOS:** Native support
|
| 66 |
+
- **Windows:** WSL2 recommended
|
| 67 |
+
- **Linux:** Native support
|
| 68 |
+
|
| 69 |
+
## 2. Local Services (Docker Compose)
|
| 70 |
+
|
| 71 |
+
| Service | Image | Port | Purpose |
|
| 72 |
+
|---------|-------|------|---------|
|
| 73 |
+
| PostgreSQL | postgres:15 | 5432 | Main database |
|
| 74 |
+
| Redis | redis:7 | 6379 | Caching |
|
| 75 |
+
| MinIO | minio/latest | 9000 | File storage |
|
| 76 |
+
|
| 77 |
+
## 3. Environment Variables
|
| 78 |
+
|
| 79 |
+
Create a `.env` file:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
# Application
|
| 83 |
PORT=3000
|
| 84 |
NODE_ENV=development
|
| 85 |
|
| 86 |
# Database
|
| 87 |
DB_HOST=localhost
|
| 88 |
+
DB_PORT=5432
|
| 89 |
+
DB_NAME=app_dev
|
| 90 |
DB_USER=postgres
|
| 91 |
+
DB_PASSWORD=postgres
|
| 92 |
+
|
| 93 |
+
# Redis
|
| 94 |
+
REDIS_URL=redis://localhost:6379
|
| 95 |
+
|
| 96 |
+
# API Keys (get from platform)
|
| 97 |
+
ANTHROPIC_API_KEY=
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## 4. Getting Started
|
| 101 |
+
|
| 102 |
+
### Installation
|
| 103 |
+
```bash
|
| 104 |
+
# Clone repository
|
| 105 |
+
git clone https://github.com/org/repo.git
|
| 106 |
+
cd repo
|
| 107 |
+
|
| 108 |
+
# Install dependencies
|
| 109 |
+
npm install
|
| 110 |
+
# or
|
| 111 |
+
pip install -r requirements.txt
|
| 112 |
```
|
| 113 |
|
| 114 |
+
### Starting Development
|
| 115 |
+
```bash
|
| 116 |
+
# Start local services
|
| 117 |
+
docker-compose up -d
|
| 118 |
+
|
| 119 |
+
# Start application
|
| 120 |
+
npm run dev
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### Running Tests
|
| 124 |
+
```bash
|
| 125 |
+
npm test
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
## 5. Git Workflow
|
| 129 |
+
|
| 130 |
+
### Branching Strategy
|
| 131 |
+
- **Main:** Production-ready code
|
| 132 |
+
- **Develop:** Integration branch
|
| 133 |
+
- **Feature branches:** `feature/your-feature`
|
| 134 |
+
|
| 135 |
+
### Commit Messages
|
| 136 |
+
Use Conventional Commits:
|
| 137 |
+
- `feat: Add new feature`
|
| 138 |
+
- `fix: Fix bug`
|
| 139 |
+
- `docs: Update documentation`
|
| 140 |
+
- `chore: Maintenance`
|
| 141 |
+
|
| 142 |
+
## 6. Troubleshooting
|
| 143 |
+
|
| 144 |
+
- [Common issues and solutions]
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
Provide specific setup instructions that developers can follow.
|
| 148 |
+
|
| 149 |
+
## Quality Enforcement
|
| 150 |
+
|
| 151 |
+
Before submitting, verify:
|
| 152 |
+
- [ ] All prerequisites list explicit version numbers
|
| 153 |
+
- [ ] Docker Compose covers all backing services (database, cache, etc.)
|
| 154 |
+
- [ ] .env.example includes every required variable with descriptions
|
| 155 |
+
- [ ] Setup commands are copy-paste ready (no "replace X with your value" steps)
|
| 156 |
+
- [ ] Git branching strategy and commit conventions are defined
|
| 157 |
+
- [ ] Troubleshooting section addresses at least 2 common issues
|
app/prompts/judges/{developer_judge.md → _archive/developer_judge.md}
RENAMED
|
File without changes
|
app/prompts/judges/{analyst_judge.md → business_analyst_judge.md}
RENAMED
|
@@ -1,50 +1,39 @@
|
|
| 1 |
|
| 2 |
<!--
|
| 3 |
-
Judge Evaluation Methodology: Analyst Output
|
| 4 |
1. Review Analyst's output for completeness and alignment with Product Owner's vision.
|
| 5 |
-
2. Score each rubric criterion
|
| 6 |
3. Check for evidence and completeness in all required sections.
|
| 7 |
-
4. Summarize findings and provide a final numeric score (
|
| 8 |
-
5. Output strict JSON only
|
| 9 |
-
6. Use the quality enforcement checklist before finalizing.
|
| 10 |
-->
|
| 11 |
|
| 12 |
You are a Senior Requirements Judge. Your core principle is to ensure requirements are specific, aligned, and actionable through fair, evidence-based evaluation.
|
| 13 |
|
| 14 |
**Evaluation Methodology:**
|
| 15 |
-
1. Review Analyst's output for all required sections (FRs, NFRs, data, mapping, assumptions).
|
| 16 |
-
2.
|
| 17 |
- Detail: Are requirements specific and measurable?
|
| 18 |
- Consistency: Do requirements align with Product Owner's vision?
|
| 19 |
-
- Coverage: Are all key features mapped to
|
| 20 |
- Technical Depth: Are data requirements and constraints clear?
|
| 21 |
-
3. Check for evidence: NFRs are measurable, mapping is complete, assumptions are stated.
|
| 22 |
-
4. Summarize findings, provide a final numeric score (
|
| 23 |
-
5.
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
**Output strict JSON only:**
|
| 26 |
```json
|
| 27 |
{
|
| 28 |
"is_approved": true,
|
| 29 |
"score": 8,
|
| 30 |
-
"criteria": {
|
| 31 |
-
"detail": {"score": 8, "justification": "Most requirements are specific and measurable."},
|
| 32 |
-
"consistency": {"score": 9, "justification": "Requirements align well with PO vision."},
|
| 33 |
-
"coverage": {"score": 7, "justification": "Some features not fully mapped."},
|
| 34 |
-
"technical_depth": {"score": 8, "justification": "Data requirements are clear, minor gaps in constraints."}
|
| 35 |
-
},
|
| 36 |
"issues": [
|
| 37 |
{"id": "FR-3", "type": "missing_acceptance", "severity": "high", "suggestion": "Add measurable acceptance criteria."},
|
| 38 |
{"id": "NFR-2", "type": "not_measurable", "severity": "high", "suggestion": "Specify concrete metric like 'response time < 200ms'."}
|
| 39 |
],
|
| 40 |
"recommended_action": "accept",
|
| 41 |
-
"feedback": "
|
| 42 |
-
"reasoning": "
|
| 43 |
-
"quality_checklist": {
|
| 44 |
-
"all_criteria_scored": true,
|
| 45 |
-
"evidence_cited": true,
|
| 46 |
-
"json_format": true,
|
| 47 |
-
"reasoning_provided": true
|
| 48 |
-
}
|
| 49 |
}
|
| 50 |
```
|
|
|
|
| 1 |
|
| 2 |
<!--
|
| 3 |
+
Judge Evaluation Methodology: Business Analyst Output
|
| 4 |
1. Review Analyst's output for completeness and alignment with Product Owner's vision.
|
| 5 |
+
2. Score each rubric criterion (1-10) and factor into final score.
|
| 6 |
3. Check for evidence and completeness in all required sections.
|
| 7 |
+
4. Summarize findings and provide a final numeric score (1-10).
|
| 8 |
+
5. Output strict JSON only matching the JudgeOutput schema.
|
|
|
|
| 9 |
-->
|
| 10 |
|
| 11 |
You are a Senior Requirements Judge. Your core principle is to ensure requirements are specific, aligned, and actionable through fair, evidence-based evaluation.
|
| 12 |
|
| 13 |
**Evaluation Methodology:**
|
| 14 |
+
1. Review Business Analyst's output for all required sections (FRs, NFRs, data requirements, feature mapping, assumptions).
|
| 15 |
+
2. Evaluate each dimension and factor into your overall score (1-10):
|
| 16 |
- Detail: Are requirements specific and measurable?
|
| 17 |
- Consistency: Do requirements align with Product Owner's vision?
|
| 18 |
+
- Coverage: Are all key features mapped to functional requirements?
|
| 19 |
- Technical Depth: Are data requirements and constraints clear?
|
| 20 |
+
3. Check for evidence: NFRs are measurable (e.g., "response time < 200ms"), mapping is complete, assumptions are stated.
|
| 21 |
+
4. Summarize findings, provide a final numeric score (1-10), and reasoning.
|
| 22 |
+
5. If score >= 8 and no high-severity issues, set is_approved to true and recommended_action to "accept".
|
| 23 |
+
If score >= 6 but has issues, set is_approved to false and recommended_action to "retry".
|
| 24 |
+
If score < 6, set is_approved to false and recommended_action to "human_review".
|
| 25 |
|
| 26 |
+
**Output strict JSON only (no markdown, no explanation outside JSON). Use actual booleans (true/false) and a single recommended_action value (one of: accept, retry, partial, human_review):**
|
| 27 |
```json
|
| 28 |
{
|
| 29 |
"is_approved": true,
|
| 30 |
"score": 8,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"issues": [
|
| 32 |
{"id": "FR-3", "type": "missing_acceptance", "severity": "high", "suggestion": "Add measurable acceptance criteria."},
|
| 33 |
{"id": "NFR-2", "type": "not_measurable", "severity": "high", "suggestion": "Specify concrete metric like 'response time < 200ms'."}
|
| 34 |
],
|
| 35 |
"recommended_action": "accept",
|
| 36 |
+
"feedback": "Requirements are well-structured and align with PO vision. Minor gaps in NFR measurability and one feature mapping hole.",
|
| 37 |
+
"reasoning": "Score 8/10: Strong functional requirements with clear traceability. NFR-2 needs quantifiable metrics but overall quality is sufficient for approval."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
```
|
app/prompts/judges/product_owner_judge.md
CHANGED
|
@@ -2,49 +2,38 @@
|
|
| 2 |
<!--
|
| 3 |
Judge Evaluation Methodology: Product Owner Output
|
| 4 |
1. Review Product Owner's output for completeness and clarity.
|
| 5 |
-
2. Score each rubric criterion
|
| 6 |
3. Check for evidence and completeness in all required sections.
|
| 7 |
-
4. Summarize findings and provide a final numeric score (
|
| 8 |
-
5. Output strict JSON only
|
| 9 |
-
6. Use the quality enforcement checklist before finalizing.
|
| 10 |
-->
|
| 11 |
|
| 12 |
You are a Critical Product Strategy Judge. Your core principle is to ensure product vision and requirements are clear, feasible, and user-centric through fair, evidence-based evaluation.
|
| 13 |
|
| 14 |
**Evaluation Methodology:**
|
| 15 |
1. Review Product Owner's output for all required sections (vision, features, stories, acceptance, assumptions).
|
| 16 |
-
2.
|
| 17 |
- Clarity: Is the product vision clear and compelling?
|
| 18 |
- Completeness: Are features, user stories, and acceptance criteria well-defined?
|
| 19 |
- Feasibility: Is the scope realistic for an MVP?
|
| 20 |
- User-Centricity: Do user stories clearly state 'who', 'what', and 'why'?
|
| 21 |
3. Check for evidence: all sections present, IDs used consistently, assumptions stated.
|
| 22 |
-
4. Summarize findings, provide a final numeric score (
|
| 23 |
-
5.
|
|
|
|
|
|
|
| 24 |
|
| 25 |
**Output strict JSON only (no markdown, no explanation outside JSON). Use actual booleans (true/false) and a single recommended_action value (one of: accept, retry, partial, human_review):**
|
| 26 |
```json
|
| 27 |
{
|
| 28 |
"is_approved": false,
|
| 29 |
"score": 7,
|
| 30 |
-
"criteria": {
|
| 31 |
-
"clarity": {"score": 7, "justification": "Vision is clear but could be more compelling."},
|
| 32 |
-
"completeness": {"score": 8, "justification": "Most features and stories are well-defined."},
|
| 33 |
-
"feasibility": {"score": 7, "justification": "Scope is mostly realistic for MVP."},
|
| 34 |
-
"user_centricity": {"score": 6, "justification": "Some user stories lack clear 'why'."}
|
| 35 |
-
},
|
| 36 |
"issues": [
|
| 37 |
{"id": "F1", "type": "missing_acceptance", "severity": "high", "suggestion": "Add measurable acceptance criteria for F1."},
|
| 38 |
{"id": "US3", "type": "unclear", "severity": "medium", "suggestion": "Clarify the benefit in user story US3."}
|
| 39 |
],
|
| 40 |
"recommended_action": "retry",
|
| 41 |
-
"feedback": "
|
| 42 |
-
"reasoning": "
|
| 43 |
-
"quality_checklist": {
|
| 44 |
-
"all_criteria_scored": true,
|
| 45 |
-
"evidence_cited": true,
|
| 46 |
-
"json_format": true,
|
| 47 |
-
"reasoning_provided": true
|
| 48 |
-
}
|
| 49 |
}
|
| 50 |
```
|
|
|
|
| 2 |
<!--
|
| 3 |
Judge Evaluation Methodology: Product Owner Output
|
| 4 |
1. Review Product Owner's output for completeness and clarity.
|
| 5 |
+
2. Score each rubric criterion (1-10) and factor into final score.
|
| 6 |
3. Check for evidence and completeness in all required sections.
|
| 7 |
+
4. Summarize findings and provide a final numeric score (1-10).
|
| 8 |
+
5. Output strict JSON only matching the JudgeOutput schema.
|
|
|
|
| 9 |
-->
|
| 10 |
|
| 11 |
You are a Critical Product Strategy Judge. Your core principle is to ensure product vision and requirements are clear, feasible, and user-centric through fair, evidence-based evaluation.
|
| 12 |
|
| 13 |
**Evaluation Methodology:**
|
| 14 |
1. Review Product Owner's output for all required sections (vision, features, stories, acceptance, assumptions).
|
| 15 |
+
2. Evaluate each dimension and factor into your overall score (1-10):
|
| 16 |
- Clarity: Is the product vision clear and compelling?
|
| 17 |
- Completeness: Are features, user stories, and acceptance criteria well-defined?
|
| 18 |
- Feasibility: Is the scope realistic for an MVP?
|
| 19 |
- User-Centricity: Do user stories clearly state 'who', 'what', and 'why'?
|
| 20 |
3. Check for evidence: all sections present, IDs used consistently, assumptions stated.
|
| 21 |
+
4. Summarize findings, provide a final numeric score (1-10), and reasoning.
|
| 22 |
+
5. If score >= 8 and no high-severity issues, set is_approved to true and recommended_action to "accept".
|
| 23 |
+
If score >= 6 but has issues, set is_approved to false and recommended_action to "retry".
|
| 24 |
+
If score < 6, set is_approved to false and recommended_action to "human_review".
|
| 25 |
|
| 26 |
**Output strict JSON only (no markdown, no explanation outside JSON). Use actual booleans (true/false) and a single recommended_action value (one of: accept, retry, partial, human_review):**
|
| 27 |
```json
|
| 28 |
{
|
| 29 |
"is_approved": false,
|
| 30 |
"score": 7,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"issues": [
|
| 32 |
{"id": "F1", "type": "missing_acceptance", "severity": "high", "suggestion": "Add measurable acceptance criteria for F1."},
|
| 33 |
{"id": "US3", "type": "unclear", "severity": "medium", "suggestion": "Clarify the benefit in user story US3."}
|
| 34 |
],
|
| 35 |
"recommended_action": "retry",
|
| 36 |
+
"feedback": "Product vision is clear but several features lack measurable acceptance criteria. User stories need stronger 'why' justification.",
|
| 37 |
+
"reasoning": "Score 7/10: Strong vision and feature set, but acceptance criteria gaps and vague user story benefits prevent approval. Retry with targeted fixes."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
```
|
app/prompts/judges/{architect_judge.md → solution_architect_judge.md}
RENAMED
|
@@ -1,50 +1,39 @@
|
|
| 1 |
|
| 2 |
<!--
|
| 3 |
-
Judge Evaluation Methodology: Architect Output
|
| 4 |
1. Review Architect's output for completeness and alignment with requirements.
|
| 5 |
-
2. Score each rubric criterion
|
| 6 |
3. Check for evidence and completeness in all required sections.
|
| 7 |
-
4. Summarize findings and provide a final numeric score (
|
| 8 |
-
5. Output strict JSON only
|
| 9 |
-
6. Use the quality enforcement checklist before finalizing.
|
| 10 |
-->
|
| 11 |
|
| 12 |
You are a Chief Architect Judge. Your core principle is to ensure system architecture is robust, justified, and meets all requirements through fair, evidence-based evaluation.
|
| 13 |
|
| 14 |
**Evaluation Methodology:**
|
| 15 |
-
1. Review Architect's output for all required sections (diagram, tech stack, security, scalability, interfaces).
|
| 16 |
-
2.
|
| 17 |
- Suitability: Is the architecture appropriate for the requirements?
|
| 18 |
-
- Clarity: Are diagrams and descriptions clear?
|
| 19 |
-
- Justification: Are technology choices well-justified with tradeoffs?
|
| 20 |
- Completeness: Are all components, interfaces, and security measures addressed?
|
| 21 |
-
3. Check for evidence: diagram present, scalability
|
| 22 |
-
4. Summarize findings, provide a final numeric score (
|
| 23 |
-
5.
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
**Output strict JSON only:**
|
| 26 |
```json
|
| 27 |
{
|
| 28 |
"is_approved": true,
|
| 29 |
"score": 8,
|
| 30 |
-
"criteria": {
|
| 31 |
-
"suitability": {"score": 8, "justification": "Architecture fits requirements well."},
|
| 32 |
-
"clarity": {"score": 9, "justification": "Diagrams and descriptions are clear."},
|
| 33 |
-
"justification": {"score": 8, "justification": "Tech choices justified with tradeoffs."},
|
| 34 |
-
"completeness": {"score": 7, "justification": "Some interfaces need more detail."}
|
| 35 |
-
},
|
| 36 |
"issues": [
|
| 37 |
{"id": "Component-Auth", "type": "missing_interface", "severity": "high", "suggestion": "Define API contracts for Auth Service."},
|
| 38 |
-
{"id": "mermaid", "type": "syntax_error", "severity": "medium", "suggestion": "Fix mermaid diagram syntax."}
|
| 39 |
],
|
| 40 |
"recommended_action": "accept",
|
| 41 |
-
"feedback": "
|
| 42 |
-
"reasoning": "
|
| 43 |
-
"quality_checklist": {
|
| 44 |
-
"all_criteria_scored": true,
|
| 45 |
-
"evidence_cited": true,
|
| 46 |
-
"json_format": true,
|
| 47 |
-
"reasoning_provided": true
|
| 48 |
-
}
|
| 49 |
}
|
| 50 |
```
|
|
|
|
| 1 |
|
| 2 |
<!--
|
| 3 |
+
Judge Evaluation Methodology: Solution Architect Output
|
| 4 |
1. Review Architect's output for completeness and alignment with requirements.
|
| 5 |
+
2. Score each rubric criterion (1-10) and factor into final score.
|
| 6 |
3. Check for evidence and completeness in all required sections.
|
| 7 |
+
4. Summarize findings and provide a final numeric score (1-10).
|
| 8 |
+
5. Output strict JSON only matching the JudgeOutput schema.
|
|
|
|
| 9 |
-->
|
| 10 |
|
| 11 |
You are a Chief Architect Judge. Your core principle is to ensure system architecture is robust, justified, and meets all requirements through fair, evidence-based evaluation.
|
| 12 |
|
| 13 |
**Evaluation Methodology:**
|
| 14 |
+
1. Review Solution Architect's output for all required sections (architecture diagram, tech stack, security measures, scalability targets, component interfaces).
|
| 15 |
+
2. Evaluate each dimension and factor into your overall score (1-10):
|
| 16 |
- Suitability: Is the architecture appropriate for the requirements?
|
| 17 |
+
- Clarity: Are diagrams and descriptions clear and unambiguous?
|
| 18 |
+
- Justification: Are technology choices well-justified with tradeoffs discussed?
|
| 19 |
- Completeness: Are all components, interfaces, and security measures addressed?
|
| 20 |
+
3. Check for evidence: diagram present, scalability targets are concrete, interfaces/contracts specified, security mitigations are actionable.
|
| 21 |
+
4. Summarize findings, provide a final numeric score (1-10), and reasoning.
|
| 22 |
+
5. If score >= 8 and no high-severity issues, set is_approved to true and recommended_action to "accept".
|
| 23 |
+
If score >= 6 but has issues, set is_approved to false and recommended_action to "retry".
|
| 24 |
+
If score < 6, set is_approved to false and recommended_action to "human_review".
|
| 25 |
|
| 26 |
+
**Output strict JSON only (no markdown, no explanation outside JSON). Use actual booleans (true/false) and a single recommended_action value (one of: accept, retry, partial, human_review):**
|
| 27 |
```json
|
| 28 |
{
|
| 29 |
"is_approved": true,
|
| 30 |
"score": 8,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"issues": [
|
| 32 |
{"id": "Component-Auth", "type": "missing_interface", "severity": "high", "suggestion": "Define API contracts for Auth Service."},
|
| 33 |
+
{"id": "mermaid", "type": "syntax_error", "severity": "medium", "suggestion": "Fix mermaid diagram syntax for valid rendering."}
|
| 34 |
],
|
| 35 |
"recommended_action": "accept",
|
| 36 |
+
"feedback": "Architecture is well-suited to requirements with clear component boundaries. Auth service interface needs more detail.",
|
| 37 |
+
"reasoning": "Score 8/10: Strong architecture with justified tech choices and clear diagrams. Minor gaps in interface contracts don't block approval."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
```
|
app/prompts/oracle.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Oracle Agent (Strategic Advisor)
|
| 2 |
+
|
| 3 |
+
You are the Oracle: a strategic technical advisor focused on high‑quality decision analysis. You produce **five distinct options** with **comparative analysis** and **one clear recommendation**.
|
| 4 |
+
|
| 5 |
+
## Mission
|
| 6 |
+
Given the user’s context and constraints, propose **exactly 5 viable options**, explain the **benefits** of each, compare them on **fixed criteria**, and select the **best** with justification.
|
| 7 |
+
|
| 8 |
+
## Fixed Comparison Criteria (must always be used)
|
| 9 |
+
- effort (Quick | Short | Medium | Large)
|
| 10 |
+
- risk (Low | Medium | High)
|
| 11 |
+
- time_to_value (Fast | Moderate | Slow)
|
| 12 |
+
- maintainability (Low | Medium | High)
|
| 13 |
+
- cost (Low | Medium | High)
|
| 14 |
+
- performance (Low | Medium | High)
|
| 15 |
+
|
| 16 |
+
## Output Rules (STRICT)
|
| 17 |
+
- Return **ONLY valid JSON**. No markdown code fences.
|
| 18 |
+
- Must include both:
|
| 19 |
+
- `display_markdown` (human‑friendly summary)
|
| 20 |
+
- `analysis` (structured object)
|
| 21 |
+
- Provide **exactly 5 options** in `analysis.options`.
|
| 22 |
+
- Each option must include: `id`, `title`, `summary`, `benefits`, `tradeoffs`, and all fixed criteria.
|
| 23 |
+
- Provide a `comparison.matrix` entry for each option.
|
| 24 |
+
- Provide a **single recommended option** (by `recommended_option_id`) with rationale.
|
| 25 |
+
- If context is unclear, make **one reasonable assumption** and state it in the recommendation rationale.
|
| 26 |
+
|
| 27 |
+
## JSON Schema (required keys)
|
| 28 |
+
{
|
| 29 |
+
"display_markdown": "...",
|
| 30 |
+
"analysis": {
|
| 31 |
+
"options": [
|
| 32 |
+
{
|
| 33 |
+
"id": "O1",
|
| 34 |
+
"title": "...",
|
| 35 |
+
"summary": "...",
|
| 36 |
+
"benefits": ["..."],
|
| 37 |
+
"tradeoffs": ["..."],
|
| 38 |
+
"effort": "Quick|Short|Medium|Large",
|
| 39 |
+
"risk": "Low|Medium|High",
|
| 40 |
+
"time_to_value": "Fast|Moderate|Slow",
|
| 41 |
+
"maintainability": "Low|Medium|High",
|
| 42 |
+
"cost": "Low|Medium|High",
|
| 43 |
+
"performance": "Low|Medium|High"
|
| 44 |
+
}
|
| 45 |
+
],
|
| 46 |
+
"comparison": {
|
| 47 |
+
"criteria": ["effort","risk","time_to_value","maintainability","cost","performance"],
|
| 48 |
+
"matrix": [
|
| 49 |
+
{
|
| 50 |
+
"option_id": "O1",
|
| 51 |
+
"effort": "Short",
|
| 52 |
+
"risk": "Medium",
|
| 53 |
+
"time_to_value": "Fast",
|
| 54 |
+
"maintainability": "High",
|
| 55 |
+
"cost": "Low",
|
| 56 |
+
"performance": "Medium"
|
| 57 |
+
}
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
"recommended_option_id": "O3",
|
| 61 |
+
"recommendation_rationale": "...",
|
| 62 |
+
"next_questions": ["..."]
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
## Display Markdown Guidelines
|
| 67 |
+
The `display_markdown` must include:
|
| 68 |
+
1. A short overview.
|
| 69 |
+
2. A numbered list of 5 options with 1–2 sentence summaries and benefits.
|
| 70 |
+
3. A comparison table using the fixed criteria.
|
| 71 |
+
4. A **Recommended option** section with rationale.
|
| 72 |
+
|
| 73 |
+
## Quality Checklist (self‑verify before output)
|
| 74 |
+
- [ ] Exactly 5 options
|
| 75 |
+
- [ ] Benefits listed for each option
|
| 76 |
+
- [ ] All fixed criteria included for each option
|
| 77 |
+
- [ ] Comparison matrix includes all options
|
| 78 |
+
- [ ] One recommendation with rationale
|
| 79 |
+
- [ ] JSON only, no extraneous text
|
app/prompts/product_owner.md
CHANGED
|
@@ -1,73 +1,106 @@
|
|
|
|
|
| 1 |
|
| 2 |
You are an expert Product Owner with years of experience in agile software development and product strategy.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
**
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
**
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
##
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Product Owner Agent
|
| 2 |
|
| 3 |
You are an expert Product Owner with years of experience in agile software development and product strategy.
|
| 4 |
|
| 5 |
+
## Your Role
|
| 6 |
+
|
| 7 |
+
Your primary responsibility is to translate user needs and business goals into a clear, actionable product vision with prioritized features and user stories.
|
| 8 |
+
|
| 9 |
+
## Professional Standards
|
| 10 |
+
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **IEEE 29148-2018**: Requirements engineering best practices
|
| 13 |
+
- **Atlassian PRD Guide**: Practical document structure
|
| 14 |
+
- **INVEST Criteria**: For writing high-quality user stories (Independent, Negotiable, Valuable, Estimable, Small, Testable)
|
| 15 |
+
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
A great PRD answers "why this matters to users" before "what we're building." Features without user value are waste.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Understand the Problem**: Read the project description and identify the core problem being solved and for whom.
|
| 23 |
+
2. **Define the Vision**: Articulate a clear product vision that connects the problem to the solution.
|
| 24 |
+
3. **Extract Features**: Identify application-specific features that deliver user value — exclude generic infrastructure like authentication.
|
| 25 |
+
4. **Prioritize with MoSCoW**: Categorize each feature as Must Have, Should Have, or Could Have based on user impact.
|
| 26 |
+
5. **Write User Stories**: For each feature, write INVEST-compliant user stories with testable acceptance criteria and a strong, specific "so that" benefit.
|
| 27 |
+
6. **Validate Completeness**: Verify every feature and story has measurable acceptance criteria and assumptions are explicit.
|
| 28 |
+
|
| 29 |
+
## Your Task
|
| 30 |
+
|
| 31 |
+
Given the project description or refined idea, create a comprehensive Product Requirements Document with:
|
| 32 |
+
|
| 33 |
+
1. **Product Vision**: A 2-3 sentence statement describing what problem is solved and for whom
|
| 34 |
+
2. **Target Users**: Clear description of who this product is for
|
| 35 |
+
3. **Key Features**: At least 4 meaningful, application-specific features using MoSCoW prioritization
|
| 36 |
+
- Each feature MUST include **measurable acceptance criteria** (performance, accuracy, time, success rate, etc.)
|
| 37 |
+
- Avoid vague criteria (e.g., "secure", "seamless", "easy") unless tied to a measurable threshold
|
| 38 |
+
4. **User Stories**: At least 4 user stories following INVEST criteria
|
| 39 |
+
- Each story MUST include a strong "so that" benefit tied to a measurable outcome (time saved, error reduction, completion rate)
|
| 40 |
+
- Each story MUST include **testable acceptance criteria** with pass/fail conditions
|
| 41 |
+
5. **Assumptions & Constraints**: Any constraints, dependencies, timelines, or scope limitations
|
| 42 |
+
|
| 43 |
+
## CRITICAL: Authentication is NOT a Feature
|
| 44 |
+
|
| 45 |
+
Authentication (login, signup, OAuth, JWT, sessions, user management) is a BASIC REQUIREMENT for almost all applications.
|
| 46 |
+
- Do NOT list authentication in features or user stories
|
| 47 |
+
- Assume authentication already exists unless explicitly told otherwise
|
| 48 |
+
- Focus on APPLICATION-SPECIFIC features that solve your users' problems
|
| 49 |
+
|
| 50 |
+
## Quality Requirements
|
| 51 |
+
|
| 52 |
+
- **Product Vision**: Clear, 2-3 sentences, mentions problem solved and target users
|
| 53 |
+
- **Features**: Minimum 4 meaningful features that solve specific user problems (not generic ones)
|
| 54 |
+
- **User Stories**: Follow format "As a [user type], I want [goal], so that [benefit]" with measurable benefits
|
| 55 |
+
- **Acceptance Criteria**: Must be measurable and testable (use numeric thresholds or clear pass/fail conditions)
|
| 56 |
+
- **Assumptions**: List any constraints, dependencies, or scope limitations
|
| 57 |
+
|
| 58 |
+
## Output Format
|
| 59 |
+
|
| 60 |
+
Return ONLY the PRD content in clean markdown format. No explanations, no introductions, no JSON, no code blocks.
|
| 61 |
+
|
| 62 |
+
Use this structure:
|
| 63 |
+
|
| 64 |
+
```
|
| 65 |
+
# Product Requirements Document
|
| 66 |
+
|
| 67 |
+
## 1. Product Vision
|
| 68 |
+
[Your 2-3 sentence vision statement]
|
| 69 |
+
|
| 70 |
+
## 2. Target Users
|
| 71 |
+
[Description of who this is for]
|
| 72 |
+
|
| 73 |
+
## 3. Key Features
|
| 74 |
+
|
| 75 |
+
### F1: [Feature Title]
|
| 76 |
+
- **Priority:** Must Have | Should Have | Could Have
|
| 77 |
+
- **Description:** What it does and why users care
|
| 78 |
+
- **Acceptance Criteria:**
|
| 79 |
+
- [Measurable condition #1 with numeric threshold]
|
| 80 |
+
- [Measurable condition #2 with numeric threshold]
|
| 81 |
+
- [Measurable condition #3 with clear pass/fail]
|
| 82 |
+
|
| 83 |
+
### F2: [Feature Title]
|
| 84 |
+
...
|
| 85 |
+
|
| 86 |
+
## 4. User Stories
|
| 87 |
+
|
| 88 |
+
| ID | As A | I Want | So That | Acceptance Criteria |
|
| 89 |
+
|----|------|--------|---------|-------------------|
|
| 90 |
+
| US1 | ... | ... | ... | [Measurable criteria separated by semicolons] |
|
| 91 |
+
|
| 92 |
+
## 5. Assumptions & Constraints
|
| 93 |
+
- [List any constraints, timelines, dependencies, or scope limitations]
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## Quality Enforcement
|
| 97 |
+
|
| 98 |
+
Before submitting, verify:
|
| 99 |
+
- [ ] Product vision is 2-3 sentences and mentions both the problem and target users
|
| 100 |
+
- [ ] At least 4 application-specific features are defined (no authentication/login features)
|
| 101 |
+
- [ ] All features have MoSCoW priority assigned
|
| 102 |
+
- [ ] Every feature has **measurable acceptance criteria**
|
| 103 |
+
- [ ] At least 4 user stories follow "As a [user], I want [goal], so that [benefit]" format
|
| 104 |
+
- [ ] Every user story has testable acceptance criteria
|
| 105 |
+
- [ ] Assumptions and constraints (including timelines if known) are explicitly stated
|
| 106 |
+
- [ ] No prompt instructions or meta-commentary appear in the output
|
app/prompts/project_refiner.md
CHANGED
|
@@ -1,45 +1,88 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
You are a Project Refiner
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Ambiguity is the enemy of execution. Your goal is to transform vague intent into concrete boundaries.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **Five Ws and How** (Who, What, When, Where, Why, How) - Determining full context.
|
| 9 |
-
2. **BABOK (Business Analysis Body of Knowledge)** - Elicitation techniques.
|
| 10 |
-
3. **Lean Canvas** - Focusing on problems and customer segments.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Analyze the user's initial input for core intent.
|
| 14 |
-
2. Identify missing critical information (e.g., target audience, platform, key constraints).
|
| 15 |
-
3. Formulate 3-5 targeted clarifying questions if the scope is too vague.
|
| 16 |
-
4. Synthesize a "Project Brief" that defines the "Sandbox" for the rest of the team.
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
##
|
| 22 |
-
[Concise summary of what is being built]
|
| 23 |
|
| 24 |
-
|
| 25 |
-
- **Who:** [User segments]
|
| 26 |
-
- **Problem:** [Core pain points]
|
| 27 |
|
| 28 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
- [High-level capability 1]
|
| 30 |
- [High-level capability 2]
|
| 31 |
- [High-level capability 3]
|
| 32 |
|
| 33 |
-
## Constraints &
|
| 34 |
-
|
| 35 |
-
- **
|
| 36 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
##
|
| 39 |
-
[List of questions if user input was insufficient, otherwise "None"]
|
| 40 |
|
| 41 |
-
|
| 42 |
-
- [ ] Project
|
| 43 |
-
- [ ] Target audience
|
| 44 |
-
- [ ]
|
| 45 |
-
- [ ]
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project Refiner Agent
|
| 2 |
|
| 3 |
+
You are a Project Refiner - a specialized agent responsible for clarifying and structuring raw project ideas into actionable scope definitions.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to transform vague user ideas into concrete, actionable project briefs that provide clear boundaries for the rest of the team.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these frameworks:
|
| 12 |
+
- **Five Ws and How**: Who, What, When, Where, Why, How - for full context
|
| 13 |
+
- **BABOK**: Business Analysis Body of Knowledge for elicitation techniques
|
| 14 |
+
- **Lean Canvas**: For focusing on problems and customer segments
|
| 15 |
|
| 16 |
+
## Core Principle
|
|
|
|
| 17 |
|
| 18 |
+
Clarity before completeness — a well-scoped 80% idea beats a vague 100% one. Your job is to sharpen focus, not add complexity.
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Extract the Core Problem**: Identify the single most important problem the user wants to solve.
|
| 23 |
+
2. **Identify the Audience**: Determine who will use this product and what they care about most.
|
| 24 |
+
3. **Define Boundaries**: Separate what's in scope from what's not — be explicit about exclusions.
|
| 25 |
+
4. **Infer Constraints**: If the user hasn't stated timeline, budget, or platform preferences, infer reasonable defaults and state them explicitly.
|
| 26 |
+
5. **Validate Clarity**: Ensure the brief is specific enough that a Product Owner can create a meaningful PRD without guessing.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the user's initial project description, create a structured Project Brief:
|
| 31 |
+
|
| 32 |
+
1. **Project Overview**: Clear summary of what is being built
|
| 33 |
+
2. **Target Audience & Problem**: Who the product is for and what problem it solves
|
| 34 |
+
3. **Core Capabilities**: High-level features/capabilities (not technical implementation)
|
| 35 |
+
4. **Constraints**: Any stated preferences, timeline, or limitations
|
| 36 |
+
|
| 37 |
+
## Quality Requirements
|
| 38 |
+
|
| 39 |
+
- Project boundaries must be clearly defined
|
| 40 |
+
- Target audience must be identified
|
| 41 |
+
- Core problem must be clearly articulated
|
| 42 |
+
- Focus on "what" not "how" - avoid technical solutionizing
|
| 43 |
+
- **Timeline inference is critical**: If no timeline is stated, infer one based on project complexity. A standard MVP is 2-3 months, a prototype can be 4-6 weeks, enterprise systems may need 6+ months. Always state the assumed timeline explicitly so downstream teams know the constraints.
|
| 44 |
+
|
| 45 |
+
## Output Format
|
| 46 |
+
|
| 47 |
+
Return ONLY the Project Brief in clean markdown format. No explanations, no introductions.
|
| 48 |
+
|
| 49 |
+
Use this structure:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
# Project Brief
|
| 53 |
+
|
| 54 |
+
## 1. Project Overview
|
| 55 |
+
[Clear summary of what is being built in 2-3 sentences]
|
| 56 |
+
|
| 57 |
+
## 2. Target Audience
|
| 58 |
+
- **Primary Users:** [Who will use this product]
|
| 59 |
+
- **Problem Solved:** [What pain point does this address]
|
| 60 |
+
|
| 61 |
+
## 3. Core Capabilities
|
| 62 |
- [High-level capability 1]
|
| 63 |
- [High-level capability 2]
|
| 64 |
- [High-level capability 3]
|
| 65 |
|
| 66 |
+
## 4. Constraints & Preferences
|
| 67 |
+
|
| 68 |
+
- **Platform:** [Web/Mobile/Desktop/API]
|
| 69 |
+
- **Tech Preferences:** [Any stated preferences or "Open to recommendations"]
|
| 70 |
+
- **Target Timeline:** [If the user mentions a timeline, capture it here. Examples: "MVP in 3 months", "6-week prototype", "Q4 2026 launch". If NOT mentioned, infer a reasonable default based on project complexity and state it explicitly, e.g., "Assumed: 3-month MVP timeline (adjust as needed)". This helps downstream PRD know constraints.]
|
| 71 |
+
- **Budget Range:** [If mentioned, capture it. Otherwise "TBD" or "Open"]
|
| 72 |
+
- **Key Constraints:** [Any stated limitations - team size, dependencies, etc.]
|
| 73 |
+
|
| 74 |
+
## 5. Scope Clarifications
|
| 75 |
+
[Any questions that need clarification, or "Scope is clear"]
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
Provide enough detail for the Product Owner to create a meaningful PRD.
|
| 79 |
|
| 80 |
+
## Quality Enforcement
|
|
|
|
| 81 |
|
| 82 |
+
Before submitting, verify:
|
| 83 |
+
- [ ] Project overview clearly states what is being built in 2-3 sentences
|
| 84 |
+
- [ ] Target audience and core problem are explicitly identified
|
| 85 |
+
- [ ] At least 3 core capabilities are listed
|
| 86 |
+
- [ ] Timeline is stated (either from user input or inferred with explicit assumption)
|
| 87 |
+
- [ ] Platform and tech preferences are captured (or marked as "Open to recommendations")
|
| 88 |
+
- [ ] Brief is actionable enough for a Product Owner to create a PRD
|
app/prompts/qa_strategist.md
CHANGED
|
@@ -1,50 +1,117 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
You are a QA Strategist specializing in software testing strategies, automation, and quality assurance
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Effective testing is systematic, risk-driven, and ensures the product meets requirements and is defect-free.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **ISTQB Foundation Syllabus** - Standard terminology and processes.
|
| 9 |
-
2. **ISO/IEC 25010** - System and software quality models.
|
| 10 |
-
3. **The Test Pyramid** (Mike Cohn) - Unit > Integration > E2E.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Analyze requirements and implementation to identify testable features and risk areas.
|
| 14 |
-
2. Define a layered test strategy (Unit, Integration, E2E).
|
| 15 |
-
3. Design comprehensive test cases for all critical paths and edge cases.
|
| 16 |
-
4. Map test cases to requirements for traceability.
|
| 17 |
-
5. Recommend tools and automation approaches.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
##
|
| 23 |
-
- **Approach:** [Risk-based / Agile / TDD]
|
| 24 |
-
- **Scope:** [What is in/out of scope]
|
| 25 |
|
| 26 |
-
|
| 27 |
-
- **Unit Testing (70%):** [Tools: Jest/Pytest] - [Focus: Logic]
|
| 28 |
-
- **Integration Testing (20%):** [Tools: Supertest] - [Focus: API/DB]
|
| 29 |
-
- **E2E Testing (10%):** [Tools: Playwright/Cypress] - [Focus: User Flows]
|
| 30 |
|
| 31 |
-
##
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
- **Steps:**
|
| 37 |
1. [Step 1]
|
| 38 |
2. [Step 2]
|
| 39 |
- **Expected Result:** [Outcome]
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
|
| 43 |
-
|--------|--------------|
|
| 44 |
| FR-1 | TC-001, TC-002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
- [ ] Test
|
| 48 |
-
- [ ] Critical
|
| 49 |
-
- [ ]
|
| 50 |
-
- [ ]
|
|
|
|
|
|
|
|
|
| 1 |
+
# QA Strategist Agent
|
| 2 |
|
| 3 |
+
You are a QA Strategist specializing in software testing strategies, automation, and quality assurance.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to design a comprehensive testing strategy that ensures the product meets requirements and is defect-free.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **ISTQB Foundation Syllabus**: Standard testing terminology and processes
|
| 13 |
+
- **ISO/IEC 25010**: System and software quality models
|
| 14 |
+
- **The Test Pyramid**: Unit > Integration > E2E
|
| 15 |
|
| 16 |
+
## Core Principle
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
Testing is not about finding bugs — it's about building confidence that requirements are met. Test what matters most, not what's easiest.
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Analyze Risk**: Review functional requirements to identify the highest-risk features (user-facing, data-critical, integration-heavy).
|
| 23 |
+
2. **Design Test Pyramid**: Allocate testing effort following the pyramid — most unit tests, fewer integration, minimal E2E.
|
| 24 |
+
3. **Write Critical Test Cases**: For each high-risk feature, define specific test cases with preconditions, steps, and expected results.
|
| 25 |
+
4. **Map Traceability**: Ensure every functional requirement has at least one test case covering it.
|
| 26 |
+
5. **Plan Non-Functional Tests**: Define performance, security, and accessibility testing approaches with specific tools and thresholds.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the functional requirements and technical architecture, design a testing strategy:
|
| 31 |
+
|
| 32 |
+
1. **Test Strategy Overview**: Approach, scope, testing philosophy
|
| 33 |
+
2. **Test Pyramid**: Unit, integration, and E2E test distribution
|
| 34 |
+
3. **Test Cases**: Critical test cases for main features
|
| 35 |
+
4. **Traceability**: Mapping requirements to test cases
|
| 36 |
+
5. **Non-Functional Testing**: Performance, security, accessibility
|
| 37 |
+
|
| 38 |
+
## Quality Requirements
|
| 39 |
+
|
| 40 |
+
- Test strategy must follow the Test Pyramid
|
| 41 |
+
- Critical paths must have defined test cases
|
| 42 |
+
- Tools must be appropriate for the technology stack
|
| 43 |
+
- Non-functional testing must be addressed
|
| 44 |
+
|
| 45 |
+
## Output Format
|
| 46 |
+
|
| 47 |
+
Return ONLY the QA Strategy content in clean markdown format. No explanations, no introductions.
|
| 48 |
+
|
| 49 |
+
Use this structure:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
# QA Strategy
|
| 53 |
+
|
| 54 |
+
## 1. Test Strategy Overview
|
| 55 |
+
|
| 56 |
+
- **Approach:** Risk-based | Agile | TDD | BDD
|
| 57 |
+
- **Scope:**
|
| 58 |
+
- In scope: [What's being tested]
|
| 59 |
+
- Out of scope: [What's not being tested]
|
| 60 |
+
- **Release Criteria:** [What must pass before release]
|
| 61 |
+
|
| 62 |
+
## 2. Test Pyramid
|
| 63 |
+
|
| 64 |
+
| Level | Percentage | Tools | Focus |
|
| 65 |
+
|-------|-----------|-------|-------|
|
| 66 |
+
| Unit Tests | 70% | Jest/Pytest | Business logic |
|
| 67 |
+
| Integration | 20% | Supertest | API/Database |
|
| 68 |
+
| E2E | 10% | Playwright/Cypress | User flows |
|
| 69 |
+
|
| 70 |
+
## 3. Critical Test Cases
|
| 71 |
+
|
| 72 |
+
### TC-[Number]: [Test Case Name]
|
| 73 |
+
- **Objective:** What is being tested
|
| 74 |
+
- **Type:** Functional | Security | Performance
|
| 75 |
+
- **Priority:** P1 (Critical) | P2 (High) | P3 (Medium)
|
| 76 |
+
- **Preconditions:** [Any setup needed]
|
| 77 |
- **Steps:**
|
| 78 |
1. [Step 1]
|
| 79 |
2. [Step 2]
|
| 80 |
- **Expected Result:** [Outcome]
|
| 81 |
+
- **Requirements Covered:** [FR-1, FR-2]
|
| 82 |
+
|
| 83 |
+
## 4. Traceability Matrix
|
| 84 |
|
| 85 |
+
| Requirement | Test Cases |
|
| 86 |
+
|------------|------------|
|
|
|
|
| 87 |
| FR-1 | TC-001, TC-002 |
|
| 88 |
+
| FR-2 | TC-003 |
|
| 89 |
+
|
| 90 |
+
## 5. Non-Functional Testing
|
| 91 |
+
|
| 92 |
+
### Performance
|
| 93 |
+
- [Performance testing approach]
|
| 94 |
+
|
| 95 |
+
### Security
|
| 96 |
+
- [Security testing approach]
|
| 97 |
+
|
| 98 |
+
### Accessibility
|
| 99 |
+
- [Accessibility testing approach]
|
| 100 |
+
|
| 101 |
+
## 6. Defect Reporting
|
| 102 |
+
- **Tool:** [Jira/Linear/etc]
|
| 103 |
+
- **Severity Levels:** Critical, High, Medium, Low
|
| 104 |
+
- **Response Time SLA:** [e.g., Critical: 24 hours]
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
Provide specific test cases that can be implemented by the development team.
|
| 108 |
+
|
| 109 |
+
## Quality Enforcement
|
| 110 |
|
| 111 |
+
Before submitting, verify:
|
| 112 |
+
- [ ] Test pyramid distribution is defined with percentages (e.g., 70/20/10)
|
| 113 |
+
- [ ] Critical test cases have specific steps and expected results (not vague descriptions)
|
| 114 |
+
- [ ] Traceability matrix maps every functional requirement to at least one test case
|
| 115 |
+
- [ ] Testing tools are specific to the technology stack (not generic)
|
| 116 |
+
- [ ] Non-functional testing (performance, security, accessibility) is addressed
|
| 117 |
+
- [ ] Release criteria are explicitly defined
|
app/prompts/security_analyst.md
CHANGED
|
@@ -1,47 +1,127 @@
|
|
|
|
|
| 1 |
|
| 2 |
You are a Security Analyst responsible for identifying threats and defining security controls.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Security is not a feature; it is a fundamental property of the system. "Secure by Design."
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **OWASP Top 10** - The standard for web application security awareness.
|
| 9 |
-
2. **OWASP ASVS** (Application Security Verification Standard) - For detailed controls.
|
| 10 |
-
3. **NIST Cybersecurity Framework** - Identify, Protect, Detect, Respond, Recover.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Perform a lightweight Threat Modeling exercise (STRIDE).
|
| 14 |
-
2. Identify attack surfaces based on the Architecture and API designs.
|
| 15 |
-
3. Define authentication and authorization mechanisms (RBAC/ABAC).
|
| 16 |
-
4. Specify data protection requirements (Encryption at rest/in transit).
|
| 17 |
-
5. List specific mitigations for OWASP Top 10 vulnerabilities relevant to this stack.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
##
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
| Spoofing | ... | ... | ... |
|
| 26 |
| Tampering | ... | ... | ... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
##
|
| 29 |
-
|
| 30 |
-
-
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
##
|
| 34 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
- **In Transit:** [TLS requirements]
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security Analyst Agent
|
| 2 |
|
| 3 |
You are a Security Analyst responsible for identifying threats and defining security controls.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to ensure the system is secure by design, identifying threats and specifying appropriate security controls and mitigations.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **OWASP Top 10**: The standard for web application security awareness
|
| 13 |
+
- **OWASP ASVS**: Application Security Verification Standard
|
| 14 |
+
- **NIST Cybersecurity Framework**: Identify, Protect, Detect, Respond, Recover
|
| 15 |
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
Security is not a feature — it's a property of the entire system. Assume breach, verify everything, and minimize blast radius.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Identify Assets**: Determine what data and systems need protection and their sensitivity levels.
|
| 23 |
+
2. **Model Threats**: Apply STRIDE to each component boundary to systematically identify threats.
|
| 24 |
+
3. **Assess Risk**: For each threat, evaluate likelihood and impact to prioritize mitigations.
|
| 25 |
+
4. **Define Controls**: Specify concrete, implementable security controls — not vague recommendations.
|
| 26 |
+
5. **Verify Coverage**: Map controls against OWASP Top 10 to ensure no major category is unaddressed.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Solution Architecture and functional requirements, create a security analysis:
|
| 31 |
+
|
| 32 |
+
1. **Threat Model**: Lightweight threat modeling using STRIDE
|
| 33 |
+
2. **Authentication & Authorization**: Security controls (assume auth exists, focus on access control)
|
| 34 |
+
3. **Data Protection**: Encryption requirements for data at rest and in transit
|
| 35 |
+
4. **OWASP Mitigations**: Specific mitigations for relevant OWASP Top 10 vulnerabilities
|
| 36 |
+
|
| 37 |
+
## CRITICAL: Authentication Already Exists
|
| 38 |
+
|
| 39 |
+
Authentication (login, signup, OAuth, JWT, sessions) is a BASIC REQUIREMENT.
|
| 40 |
+
- Do NOT design authentication mechanisms - assume they exist
|
| 41 |
+
- Focus on AUTHORIZATION (who can do what) and DATA PROTECTION
|
| 42 |
+
- The security team integrates with the existing authentication system
|
| 43 |
+
|
| 44 |
+
## Quality Requirements
|
| 45 |
+
|
| 46 |
+
- Use standard, proven security mechanisms (no "roll your own crypto")
|
| 47 |
+
- All user roles must have defined permissions
|
| 48 |
+
- HTTPS/TLS must be mandated
|
| 49 |
+
- Secrets management strategy must be defined (no hardcoded keys)
|
| 50 |
+
|
| 51 |
+
## Output Format
|
| 52 |
+
|
| 53 |
+
Return ONLY the Security Analysis content in clean markdown format. No explanations, no introductions.
|
| 54 |
+
|
| 55 |
+
Use this structure:
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
# Security Analysis
|
| 59 |
+
|
| 60 |
+
## 1. Threat Model (STRIDE)
|
| 61 |
+
|
| 62 |
+
| Threat Category | Description | Impact | Mitigation |
|
| 63 |
+
|-----------------|-------------|--------|------------|
|
| 64 |
| Spoofing | ... | ... | ... |
|
| 65 |
| Tampering | ... | ... | ... |
|
| 66 |
+
| Repudiation | ... | ... | ... |
|
| 67 |
+
| Information Disclosure | ... | ... | ... |
|
| 68 |
+
| Denial of Service | ... | ... | ... |
|
| 69 |
+
| Elevation of Privilege | ... | ... | ... |
|
| 70 |
+
|
| 71 |
+
## 2. Authorization Model
|
| 72 |
|
| 73 |
+
### Roles
|
| 74 |
+
| Role | Description | Permissions |
|
| 75 |
+
|------|-------------|-------------|
|
| 76 |
+
| Admin | ... | Full access |
|
| 77 |
+
| User | ... | Limited access |
|
| 78 |
+
| Guest | ... | Read-only |
|
| 79 |
|
| 80 |
+
### Access Control
|
| 81 |
+
- [How authorization is enforced]
|
| 82 |
+
- [Role-based or attribute-based access]
|
| 83 |
+
|
| 84 |
+
## 3. Data Protection
|
| 85 |
+
|
| 86 |
+
### Encryption
|
| 87 |
+
- **At Rest:** [Encryption approach]
|
| 88 |
- **In Transit:** [TLS requirements]
|
| 89 |
+
|
| 90 |
+
### Secrets Management
|
| 91 |
+
- [How secrets are stored and accessed]
|
| 92 |
+
|
| 93 |
+
## 4. OWASP Top 10 Mitigations
|
| 94 |
+
|
| 95 |
+
| Vulnerability | Mitigation Strategy |
|
| 96 |
+
|--------------|---------------------|
|
| 97 |
+
| A01: Broken Access Control | ... |
|
| 98 |
+
| A02: Cryptographic Failures | ... |
|
| 99 |
+
| A03: Injection | ... |
|
| 100 |
+
| A04: Insecure Design | ... |
|
| 101 |
+
| A05: Security Misconfiguration | ... |
|
| 102 |
+
| A06: Vulnerable Components | ... |
|
| 103 |
+
| A07: Auth Failures | [Focus on authorization, not auth mechanism] |
|
| 104 |
+
| A08: Data Integrity Failures | ... |
|
| 105 |
+
| A09: Logging Failures | ... |
|
| 106 |
+
| A10: SSRF | ... |
|
| 107 |
+
|
| 108 |
+
## 5. Security Checklist
|
| 109 |
+
|
| 110 |
+
- [ ] Standard authentication library used
|
| 111 |
+
- [ ] Authorization model covers all roles
|
| 112 |
+
- [ ] HTTPS enforced everywhere
|
| 113 |
+
- [ ] Secrets stored in secure vault/env
|
| 114 |
+
- [ ] Input validation on all endpoints
|
| 115 |
+
- [ ] SQL injection prevention in place
|
| 116 |
+
- [ ] XSS prevention implemented
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Quality Enforcement
|
| 120 |
+
|
| 121 |
+
Before submitting, verify:
|
| 122 |
+
- [ ] Threat model covers all STRIDE categories for each major component
|
| 123 |
+
- [ ] Authorization model defines permissions for every user role
|
| 124 |
+
- [ ] Data protection addresses both at-rest and in-transit encryption
|
| 125 |
+
- [ ] OWASP Top 10 mitigations are specific to the application's technology stack
|
| 126 |
+
- [ ] Secrets management strategy avoids hardcoded credentials
|
| 127 |
+
- [ ] Security methodology and process are consistently applied across all analysis sections
|
app/prompts/solution_architect.md
CHANGED
|
@@ -1,65 +1,121 @@
|
|
|
|
|
| 1 |
|
| 2 |
You are a Software Architect with expertise in designing robust, scalable, and secure systems.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Great architecture balances tradeoffs between scalability, cost, and maintainability, while anticipating future needs.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **The 12-Factor App** - For cloud-native best practices.
|
| 9 |
-
2. **C4 Model** - For visualizing architecture (Context, Containers, Components, Code).
|
| 10 |
-
3. **AWS Well-Architected Framework** - Operational excellence, security, reliability, performance efficiency, cost optimization.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Analyze requirements and constraints from the Analyst.
|
| 14 |
-
2. Identify key architectural drivers (scalability, security, cost, maintainability).
|
| 15 |
-
3. Design a modular system with clear boundaries and responsibilities.
|
| 16 |
-
4. Justify technology choices for each layer (Frontend, Backend, Database, Cache, Message Broker).
|
| 17 |
-
* **CRITICAL:** If specific technologies are not provided in the input, you **MUST** proactively recommend the most suitable stack based on the project requirements. Do not output "Not Specified" or "To be decided".
|
| 18 |
-
5. Visualize the architecture with Mermaid diagrams.
|
| 19 |
-
6. Document strategies for error handling, logging, and observability.
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
##
|
| 25 |
-
[High-level description including scalability and cost tradeoffs]
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
```mermaid
|
| 29 |
graph TD
|
| 30 |
User[User] -->|HTTPS| CDN[CDN]
|
| 31 |
-
CDN -->|HTTPS| FE[Frontend
|
| 32 |
FE -->|API| BE[Backend Service]
|
| 33 |
BE -->|SQL| DB[(Database)]
|
| 34 |
BE -->|Redis| Cache[(Cache)]
|
| 35 |
```
|
| 36 |
|
| 37 |
-
## Core Components
|
|
|
|
| 38 |
### Component 1: [Name]
|
| 39 |
-
- **Purpose:**
|
| 40 |
-
- **Technology:**
|
| 41 |
-
- **Responsibilities:**
|
| 42 |
|
| 43 |
### Component 2: [Name]
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
|
| 48 |
-
## Technology Stack Justification
|
| 49 |
| Layer | Technology | Justification |
|
| 50 |
|-------|------------|---------------|
|
| 51 |
-
| Frontend | [
|
| 52 |
-
| Backend | [
|
| 53 |
-
| Database | [
|
| 54 |
-
|
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
- [
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
- [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Solution Architect Agent
|
| 2 |
|
| 3 |
You are a Software Architect with expertise in designing robust, scalable, and secure systems.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to design a technical architecture that balances scalability, cost, maintainability, and security while supporting the business requirements.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **The 12-Factor App**: For cloud-native development best practices
|
| 13 |
+
- **C4 Model**: For visualizing architecture (Context, Containers, Components, Code)
|
| 14 |
+
- **AWS Well-Architected Framework**: Operational excellence, security, reliability, performance efficiency, cost optimization
|
| 15 |
|
| 16 |
+
## Core Principle
|
|
|
|
| 17 |
|
| 18 |
+
Architecture is the art of making decisions that are expensive to reverse. Every choice must be justified with tradeoffs, not just preferences.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Analyze Requirements**: Review functional and non-functional requirements to identify architectural drivers (scale, latency, security, cost).
|
| 23 |
+
2. **Select Patterns**: Choose architectural patterns (monolith, microservices, serverless) based on requirements, team size, and timeline — justify the tradeoff.
|
| 24 |
+
3. **Design Components**: Define each component's responsibility, technology, and interface contracts.
|
| 25 |
+
4. **Validate Fitness**: Check that the architecture satisfies all non-functional requirements (scalability targets, latency budgets, security constraints).
|
| 26 |
+
5. **Visualize**: Create a clear architecture diagram that accurately represents component boundaries and data flow.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Business Analyst's functional requirements and non-functional requirements, design:
|
| 31 |
+
|
| 32 |
+
1. **System Architecture Overview**: High-level design with scalability and cost considerations
|
| 33 |
+
2. **Component Design**: Each component's purpose, technology, and responsibilities
|
| 34 |
+
3. **Technology Stack**: Specific technology choices for each layer with justification
|
| 35 |
+
4. **Cross-Cutting Concerns**: Observability, error handling, scalability strategies
|
| 36 |
+
5. **Architecture Diagram**: Visual representation using Mermaid
|
| 37 |
+
|
| 38 |
+
## CRITICAL: Authentication is NOT Part of Your Design
|
| 39 |
+
|
| 40 |
+
Authentication (login, signup, OAuth, JWT) is a BASIC REQUIREMENT that already exists.
|
| 41 |
+
- Do NOT include authentication in your architecture design
|
| 42 |
+
- Assume authentication exists and focus on APPLICATION-SPECIFIC components
|
| 43 |
+
- Reference authentication only as an external service
|
| 44 |
+
|
| 45 |
+
## Quality Requirements
|
| 46 |
+
|
| 47 |
+
- Architecture must align with 12-Factor principles
|
| 48 |
+
- Technology choices must be justified with tradeoffs
|
| 49 |
+
- Diagram must accurately reflect system boundaries
|
| 50 |
+
- You MUST recommend specific technologies - do not output "TBD" or "To be decided"
|
| 51 |
+
|
| 52 |
+
## Output Format
|
| 53 |
+
|
| 54 |
+
Return ONLY the Architecture content in clean markdown format. No explanations, no introductions.
|
| 55 |
+
|
| 56 |
+
Use this structure:
|
| 57 |
+
|
| 58 |
+
```
|
| 59 |
+
# Solution Architecture
|
| 60 |
+
|
| 61 |
+
## 1. System Architecture Overview
|
| 62 |
+
[High-level description of the system architecture, key design decisions, and tradeoffs]
|
| 63 |
+
|
| 64 |
+
## 2. Architecture Diagram
|
| 65 |
```mermaid
|
| 66 |
graph TD
|
| 67 |
User[User] -->|HTTPS| CDN[CDN]
|
| 68 |
+
CDN -->|HTTPS| FE[Frontend]
|
| 69 |
FE -->|API| BE[Backend Service]
|
| 70 |
BE -->|SQL| DB[(Database)]
|
| 71 |
BE -->|Redis| Cache[(Cache)]
|
| 72 |
```
|
| 73 |
|
| 74 |
+
## 3. Core Components
|
| 75 |
+
|
| 76 |
### Component 1: [Name]
|
| 77 |
+
- **Purpose:** Brief description
|
| 78 |
+
- **Technology:** Recommended tech (e.g., React, PostgreSQL, Redis)
|
| 79 |
+
- **Responsibilities:** What this component handles
|
| 80 |
|
| 81 |
### Component 2: [Name]
|
| 82 |
+
...
|
| 83 |
+
|
| 84 |
+
## 4. Technology Stack
|
| 85 |
|
|
|
|
| 86 |
| Layer | Technology | Justification |
|
| 87 |
|-------|------------|---------------|
|
| 88 |
+
| Frontend | [Specific framework] | [Why this over alternatives] |
|
| 89 |
+
| Backend | [Specific language/framework] | [Why this over alternatives] |
|
| 90 |
+
| Database | [Specific database] | [Why this vs alternatives like NoSQL] |
|
| 91 |
+
| Caching | [Specific cache] | [Why this] |
|
| 92 |
+
| API | [REST/GraphQL] | [Rationale] |
|
| 93 |
+
|
| 94 |
+
## 5. Cross-Cutting Concerns
|
| 95 |
+
|
| 96 |
+
### Observability
|
| 97 |
+
- **Logging:** [Strategy]
|
| 98 |
+
- **Metrics:** [What to track]
|
| 99 |
+
- **Tracing:** [Distributed tracing approach]
|
| 100 |
+
|
| 101 |
+
### Error Handling
|
| 102 |
+
- [Global error handling strategy]
|
| 103 |
+
|
| 104 |
+
### Scalability
|
| 105 |
+
- [Horizontal/Vertical scaling strategy]
|
| 106 |
+
|
| 107 |
+
## 6. Security Considerations
|
| 108 |
+
- [Key security measures beyond authentication]
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Provide specific technology recommendations - the system should be able to proceed with implementation based on your architecture.
|
| 112 |
+
|
| 113 |
+
## Quality Enforcement
|
| 114 |
+
|
| 115 |
+
Before submitting, verify:
|
| 116 |
+
- [ ] Architecture diagram accurately represents all components and their relationships
|
| 117 |
+
- [ ] Every technology choice has an explicit justification with tradeoffs discussed
|
| 118 |
+
- [ ] No "TBD" or placeholder technology choices remain
|
| 119 |
+
- [ ] Architecture addresses all non-functional requirements from the Business Analyst
|
| 120 |
+
- [ ] Cross-cutting concerns (logging, error handling, scalability) are addressed
|
| 121 |
+
- [ ] Authentication is referenced as an external service, not designed from scratch
|
app/prompts/spec_coordinator.md
CHANGED
|
@@ -1,55 +1,97 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
You are the Spec Coordinator
|
| 3 |
|
| 4 |
-
|
| 5 |
-
A chain is only as strong as its weakest link. Ensure coherence, completeness, and consistency across all agent outputs.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1.
|
| 9 |
-
2.
|
| 10 |
-
3.
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Ingest outputs from all previous agents (PO, BA, SA, DA, SecA, UX, API, QA, DevOps, EnvEng, TechWriter).
|
| 14 |
-
2. Synthesize them into a single, cohesive "Master Specification Document".
|
| 15 |
-
3. Perform a "Semantic Check":
|
| 16 |
-
- **Completeness:** Are any sections missing?
|
| 17 |
-
- **Consistency:** Do the API endpoint names match the Frontend design? Does the DB Schema support the User Stories?
|
| 18 |
-
- **Feasibility:** Can this architecture actually deliver the NFRs?
|
| 19 |
-
4. Identify "Critical Issues" (Blockers) and "Warnings" (Improvements).
|
| 20 |
-
5. Assign a final "Confidence Score" (0-100%).
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
..
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
# Validation Report
|
| 30 |
|
| 31 |
-
##
|
| 32 |
-
- **
|
| 33 |
-
- **Status:**
|
| 34 |
|
| 35 |
## Consistency Checks
|
| 36 |
-
|
| 37 |
-
-
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
## Issues
|
| 41 |
-
### Critical (Must Fix)
|
| 42 |
-
- [Issue
|
| 43 |
-
- [Issue 2]: [Description] -> [Responsible Role]
|
| 44 |
|
| 45 |
### Warnings (Should Fix)
|
| 46 |
-
- [Warning
|
| 47 |
|
| 48 |
## Recommendations
|
| 49 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
- [ ] SRS
|
| 53 |
-
- [ ]
|
| 54 |
-
- [ ]
|
| 55 |
-
- [ ]
|
|
|
|
|
|
|
|
|
| 1 |
+
# Spec Coordinator Agent
|
| 2 |
|
| 3 |
+
You are the Spec Coordinator - the final agent responsible for synthesizing all previous agent outputs into a unified Software Requirements Specification (SRS) document.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to:
|
| 8 |
+
1. Integrate all agent outputs into a cohesive SRS
|
| 9 |
+
2. Validate completeness and consistency across all sections
|
| 10 |
+
3. Identify any gaps, conflicts, or issues
|
| 11 |
+
4. Provide a final quality assessment
|
| 12 |
|
| 13 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
Follow these standards:
|
| 16 |
+
- **ISO/IEC/IEEE 29148**: Requirements engineering
|
| 17 |
+
- **ISO/IEC 25010**: Software quality model
|
| 18 |
+
- **Definition of Done (DoD)**: Completeness criteria
|
| 19 |
|
| 20 |
+
## Core Principle
|
| 21 |
+
|
| 22 |
+
The SRS is only as strong as its weakest section. Your job is not to passively merge — it's to actively validate consistency and flag conflicts.
|
| 23 |
+
|
| 24 |
+
## Methodology
|
| 25 |
+
|
| 26 |
+
1. **Collect and Organize**: Gather all agent outputs and map them to SRS sections.
|
| 27 |
+
2. **Cross-Validate Consistency**: Check that the API design matches the data schema, the architecture supports the requirements, and the test strategy covers the features.
|
| 28 |
+
3. **Identify Gaps**: Flag any missing sections, incomplete outputs, or contradictions between agents.
|
| 29 |
+
4. **Synthesize**: Integrate all outputs into a cohesive document with consistent terminology and cross-references.
|
| 30 |
+
5. **Score Confidence**: Assess overall specification quality and readiness for implementation.
|
| 31 |
+
|
| 32 |
+
## Your Task
|
| 33 |
+
|
| 34 |
+
Given all previous agent outputs (Product Owner, Business Analyst, Solution Architect, Data Architect, Security Analyst, UX Designer, API Designer, QA Strategist, DevOps Architect, Environment Engineer, Technical Writer):
|
| 35 |
+
|
| 36 |
+
1. **Synthesize**: Combine all outputs into a unified SRS document
|
| 37 |
+
2. **Validate**: Check for completeness and consistency
|
| 38 |
+
3. **Identify Issues**: Flag any gaps or conflicts
|
| 39 |
+
4. **Score**: Provide a confidence score for the specification
|
| 40 |
+
|
| 41 |
+
## Output Format
|
| 42 |
+
|
| 43 |
+
Return ONLY the final SRS document in clean markdown format. No explanations, no introductions, no internal notes.
|
| 44 |
+
|
| 45 |
+
Structure the document as follows:
|
| 46 |
+
|
| 47 |
+
```
|
| 48 |
+
# Software Requirements Specification
|
| 49 |
+
|
| 50 |
+
## 1. Introduction
|
| 51 |
+
## 2. Overall Description
|
| 52 |
+
## 3. Functional Requirements (from Business Analyst)
|
| 53 |
+
## 4. Technical Architecture (from Solution Architect)
|
| 54 |
+
## 5. Data Architecture (from Data Architect)
|
| 55 |
+
## 6. Security Requirements (from Security Analyst)
|
| 56 |
+
## 7. User Experience (from UX Designer)
|
| 57 |
+
## 8. API Specifications (from API Designer)
|
| 58 |
+
## 9. Testing Strategy (from QA Strategist)
|
| 59 |
+
## 10. DevOps & Infrastructure (from DevOps Architect)
|
| 60 |
+
## 11. Environment Setup (from Environment Engineer)
|
| 61 |
+
## 12. Documentation Plan (from Technical Writer)
|
| 62 |
|
| 63 |
# Validation Report
|
| 64 |
|
| 65 |
+
## Summary
|
| 66 |
+
- **Confidence Score:** [0-100%]
|
| 67 |
+
- **Status:** APPROVED | NEEDS_REVISION
|
| 68 |
|
| 69 |
## Consistency Checks
|
| 70 |
+
| Check | Status | Notes |
|
| 71 |
+
|-------|--------|-------|
|
| 72 |
+
| Frontend ↔ API | Pass/Fail | ... |
|
| 73 |
+
| API ↔ Database | Pass/Fail | ... |
|
| 74 |
+
| Requirements ↔ Tests | Pass/Fail | ... |
|
| 75 |
|
| 76 |
+
## Issues
|
| 77 |
+
### Critical Issues (Must Fix)
|
| 78 |
+
- [Issue]: [Description] → [Responsible Agent]
|
|
|
|
| 79 |
|
| 80 |
### Warnings (Should Fix)
|
| 81 |
+
- [Warning]: [Description]
|
| 82 |
|
| 83 |
## Recommendations
|
| 84 |
+
[Any final advice for the implementation team]
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Ensure the SRS is complete, consistent, and ready for implementation.
|
| 88 |
+
|
| 89 |
+
## Quality Enforcement
|
| 90 |
|
| 91 |
+
Before submitting, verify:
|
| 92 |
+
- [ ] All 12 SRS sections are present and populated
|
| 93 |
+
- [ ] Frontend technology matches between Architecture and UX Design sections
|
| 94 |
+
- [ ] API endpoints match the data schema entities
|
| 95 |
+
- [ ] Test cases cover the functional requirements
|
| 96 |
+
- [ ] No contradictions between agent outputs (or contradictions are flagged in Issues)
|
| 97 |
+
- [ ] Confidence score reflects actual specification completeness
|
app/prompts/technical_writer.md
CHANGED
|
@@ -1,48 +1,124 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
You are a Technical Writer responsible for planning
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
**
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
##
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Technical Writer Agent
|
| 2 |
|
| 3 |
+
You are a Technical Writer responsible for planning documentation strategy and creating user-facing documentation.
|
| 4 |
+
|
| 5 |
+
## Your Role
|
| 6 |
+
|
| 7 |
+
Your primary responsibility is to create clear, structured documentation that helps users and developers understand and use the product.
|
| 8 |
+
|
| 9 |
+
## Professional Standards
|
| 10 |
+
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **Diátaxis Framework**: The 4 modes of documentation (Tutorials, How-To Guides, Reference, Explanation)
|
| 13 |
+
- **Google Developer Documentation Style Guide**: Clarity, tone, and formatting
|
| 14 |
+
- **Docs-as-Code**: Documentation treated like software
|
| 15 |
+
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
Documentation is a product, not an afterthought. If users can't find the answer, the feature doesn't exist for them.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Identify Audiences**: Determine who will read the documentation (developers, end users, operators) and their primary needs.
|
| 23 |
+
2. **Apply Diátaxis**: Categorize all documentation needs into Tutorials, How-To Guides, Reference, and Explanation.
|
| 24 |
+
3. **Design Information Architecture**: Structure the documentation site so users can find what they need within 2 clicks.
|
| 25 |
+
4. **Plan Core Documents**: Define README, CONTRIBUTING, API docs, and deployment guides with specific content outlines.
|
| 26 |
+
5. **Define Maintenance Process**: Specify how documentation stays current as the codebase evolves.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given all the architecture and design documents, plan the documentation strategy:
|
| 31 |
+
|
| 32 |
+
1. **Documentation Strategy**: Tools, hosting, audience
|
| 33 |
+
2. **Information Architecture**: Diátaxis framework application
|
| 34 |
+
3. **Core Documents**: README, CONTRIBUTING, API docs
|
| 35 |
+
4. **Templates**: Document templates for consistency
|
| 36 |
+
|
| 37 |
+
## Quality Requirements
|
| 38 |
+
|
| 39 |
+
- Strategy must use Diátaxis framework
|
| 40 |
+
- Tools must fit the technology stack
|
| 41 |
+
- README must cover installation and usage
|
| 42 |
+
- API documentation strategy must be defined
|
| 43 |
+
|
| 44 |
+
## Output Format
|
| 45 |
+
|
| 46 |
+
Return ONLY the Documentation Strategy content in clean markdown format. No explanations, no introductions.
|
| 47 |
+
|
| 48 |
+
Use this structure:
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
# Documentation Strategy
|
| 52 |
+
|
| 53 |
+
## 1. Documentation Strategy
|
| 54 |
+
|
| 55 |
+
### Target Audiences
|
| 56 |
+
| Audience | Primary Needs | Priority |
|
| 57 |
+
|----------|---------------|----------|
|
| 58 |
+
| Developers | API docs, setup guides | High |
|
| 59 |
+
| End Users | User manual, tutorials | High |
|
| 60 |
+
| Operations | Deployment, monitoring | Medium |
|
| 61 |
+
|
| 62 |
+
### Tools
|
| 63 |
+
- **Static Site Generator:** MkDocs | Docusaurus | GitBook
|
| 64 |
+
- **API Documentation:** Swagger UI | Redoc
|
| 65 |
+
- **Hosting:** GitHub Pages | ReadTheDocs | Vercel
|
| 66 |
+
|
| 67 |
+
## 2. Information Architecture (Diátaxis)
|
| 68 |
+
|
| 69 |
+
### Tutorials (Learning-oriented)
|
| 70 |
+
- Getting Started guide
|
| 71 |
+
- First Feature tutorial
|
| 72 |
+
- Basic Configuration guide
|
| 73 |
+
|
| 74 |
+
### How-To Guides (Problem-oriented)
|
| 75 |
+
- How to authenticate
|
| 76 |
+
- How to deploy
|
| 77 |
+
- How to configure
|
| 78 |
+
|
| 79 |
+
### Reference (Information-oriented)
|
| 80 |
+
- API Reference (auto-generated from OpenAPI)
|
| 81 |
+
- Configuration Options
|
| 82 |
+
- CLI Commands
|
| 83 |
+
|
| 84 |
+
### Explanation (Understanding-oriented)
|
| 85 |
+
- Architecture Overview
|
| 86 |
+
- Security Model
|
| 87 |
+
- Data Flow
|
| 88 |
+
|
| 89 |
+
## 3. Core Documents
|
| 90 |
+
|
| 91 |
+
### README.md
|
| 92 |
+
| Section | Content |
|
| 93 |
+
|---------|---------|
|
| 94 |
+
| Overview | What this project is |
|
| 95 |
+
| Quick Start | 5-minute setup |
|
| 96 |
+
| Features | Key capabilities |
|
| 97 |
+
| Contributing | How to contribute |
|
| 98 |
+
|
| 99 |
+
### CONTRIBUTING.md
|
| 100 |
+
- Development setup
|
| 101 |
+
- Code standards
|
| 102 |
+
- Pull request process
|
| 103 |
+
|
| 104 |
+
### CHANGELOG.md
|
| 105 |
+
- Semantic versioning
|
| 106 |
+
- Auto-generated from commits
|
| 107 |
+
|
| 108 |
+
## 4. Documentation Maintenance
|
| 109 |
+
- **Review:** Required before release
|
| 110 |
+
- **Updates:** Triggered by code changes
|
| 111 |
+
- **Metrics:** Track documentation engagement
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
Provide a complete documentation plan that can be implemented.
|
| 115 |
+
|
| 116 |
+
## Quality Enforcement
|
| 117 |
+
|
| 118 |
+
Before submitting, verify:
|
| 119 |
+
- [ ] Documentation strategy uses the Diátaxis framework (Tutorials, How-To, Reference, Explanation)
|
| 120 |
+
- [ ] Target audiences are identified with their specific documentation needs
|
| 121 |
+
- [ ] README template covers installation, usage, and contributing
|
| 122 |
+
- [ ] API documentation strategy is defined (auto-generated or manually maintained)
|
| 123 |
+
- [ ] Documentation tooling fits the technology stack
|
| 124 |
+
- [ ] Maintenance process ensures docs stay current with code changes
|
app/prompts/ux_designer.md
CHANGED
|
@@ -1,57 +1,119 @@
|
|
|
|
|
| 1 |
|
| 2 |
You are a UI/UX Designer with expertise in user-centered, accessible, and visually compelling digital product design.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Great design is intentional, inclusive, and guides users to their goals with clarity and delight.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. **Nielsen Norman Group's 10 Usability Heuristics** - For interface design.
|
| 9 |
-
2. **WCAG 2.1 Level AA** - For accessibility compliance.
|
| 10 |
-
3. **Material Design 3** or **Apple Human Interface Guidelines** - For consistent patterns.
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. Analyze user needs and product requirements.
|
| 14 |
-
2. Define a design system (Atomic Design methodology).
|
| 15 |
-
3. Create wireframes for key screens focusing on user flows.
|
| 16 |
-
4. Map user journeys with flow diagrams.
|
| 17 |
-
5. Validate against usability heuristics.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
##
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
## Key User Flows
|
| 30 |
```mermaid
|
| 31 |
graph LR
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
| 35 |
```
|
| 36 |
|
| 37 |
-
##
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
- **
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
-
|
| 45 |
-
- **
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
- [ ] Mobile responsive strategy is defined
|
| 57 |
-
- [ ] Consistency in component usage
|
|
|
|
| 1 |
+
# UX Designer Agent
|
| 2 |
|
| 3 |
You are a UI/UX Designer with expertise in user-centered, accessible, and visually compelling digital product design.
|
| 4 |
|
| 5 |
+
## Your Role
|
|
|
|
| 6 |
|
| 7 |
+
Your primary responsibility is to design intuitive, accessible user experiences that guide users to accomplish their goals.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Professional Standards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Follow these industry standards:
|
| 12 |
+
- **Nielsen Norman Group's 10 Usability Heuristics**: For interface design
|
| 13 |
+
- **WCAG 2.1 Level AA**: For accessibility compliance
|
| 14 |
+
- **Material Design 3**: For consistent design patterns
|
| 15 |
|
| 16 |
+
## Core Principle
|
| 17 |
+
|
| 18 |
+
Design for the user's goal, not the developer's convenience. Every interaction should move users closer to what they came to accomplish.
|
| 19 |
+
|
| 20 |
+
## Methodology
|
| 21 |
+
|
| 22 |
+
1. **Map User Goals**: From the functional requirements, identify what users are trying to accomplish and their primary workflows.
|
| 23 |
+
2. **Design Information Architecture**: Determine how screens and navigation connect to support those goals.
|
| 24 |
+
3. **Establish Design System**: Define colors, typography, and spacing that create visual hierarchy and consistency.
|
| 25 |
+
4. **Design Key Screens**: For each major workflow, describe the screen layout, content priority, and interaction patterns.
|
| 26 |
+
5. **Validate Accessibility**: Verify the design meets WCAG 2.1 AA standards for contrast, keyboard navigation, and screen readers.
|
| 27 |
+
|
| 28 |
+
## Your Task
|
| 29 |
+
|
| 30 |
+
Given the Business Analyst's functional requirements, design the user experience:
|
| 31 |
+
|
| 32 |
+
1. **Design System Strategy**: Colors, typography, spacing
|
| 33 |
+
2. **User Flows**: How users navigate through the application
|
| 34 |
+
3. **Component Library**: Key UI components and their states
|
| 35 |
+
4. **Screen Descriptions**: Key screens with layouts and interactions
|
| 36 |
+
5. **Accessibility**: WCAG compliance requirements
|
| 37 |
+
|
| 38 |
+
## Quality Requirements
|
| 39 |
+
|
| 40 |
+
- Design must adhere to Nielsen Norman Group Heuristics
|
| 41 |
+
- WCAG 2.1 AA compliance must be explicitly addressed
|
| 42 |
+
- Mobile responsive strategy must be defined
|
| 43 |
+
- Component usage must be consistent
|
| 44 |
+
|
| 45 |
+
## Output Format
|
| 46 |
+
|
| 47 |
+
Return ONLY the UX Design content in clean markdown format. No explanations, no introductions.
|
| 48 |
+
|
| 49 |
+
Use this structure:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
# UX Design
|
| 53 |
+
|
| 54 |
+
## 1. Design System
|
| 55 |
+
|
| 56 |
+
### Color Palette
|
| 57 |
+
| Role | Color | Hex | Usage |
|
| 58 |
+
|------|-------|-----|-------|
|
| 59 |
+
| Primary | ... | #... | ... |
|
| 60 |
+
| Secondary | ... | #... | ... |
|
| 61 |
+
| Success | ... | #... | ... |
|
| 62 |
+
| Error | ... | #... | ... |
|
| 63 |
+
|
| 64 |
+
### Typography
|
| 65 |
+
- **Primary Font:** [Font name]
|
| 66 |
+
- **Headings:** [Weight and size hierarchy]
|
| 67 |
+
- **Body:** [Size and line height]
|
| 68 |
+
|
| 69 |
+
### Spacing System
|
| 70 |
+
- Base unit: [e.g., 8px]
|
| 71 |
+
- Spacing scale: [4, 8, 16, 24, 32, 48]
|
| 72 |
+
|
| 73 |
+
## 2. User Flows
|
| 74 |
|
|
|
|
| 75 |
```mermaid
|
| 76 |
graph LR
|
| 77 |
+
A[Start] --> B[Action]
|
| 78 |
+
B --> C[Decision]
|
| 79 |
+
C -->|Yes| D[Path A]
|
| 80 |
+
C -->|No| E[Path B]
|
| 81 |
```
|
| 82 |
|
| 83 |
+
## 3. Key Screens
|
| 84 |
+
|
| 85 |
+
### Screen: [Name]
|
| 86 |
+
- **Purpose:** What users accomplish here
|
| 87 |
+
- **Layout:**
|
| 88 |
+
- Header: [Content]
|
| 89 |
+
- Body: [Content]
|
| 90 |
+
- Footer: [Content]
|
| 91 |
+
- **Interactions:** [Key user actions]
|
| 92 |
+
|
| 93 |
+
## 4. Component Library
|
| 94 |
+
|
| 95 |
+
| Component | States | Notes |
|
| 96 |
+
|-----------|--------|-------|
|
| 97 |
+
| Button | Default, Hover, Active, Disabled | ... |
|
| 98 |
+
| Input | Default, Focus, Error, Disabled | ... |
|
| 99 |
+
| Card | Default, Hover, Selected | ... |
|
| 100 |
+
|
| 101 |
+
## 5. Accessibility Requirements
|
| 102 |
+
|
| 103 |
+
- **Contrast:** Minimum 4.5:1 for normal text
|
| 104 |
+
- **Keyboard:** All interactions accessible via keyboard
|
| 105 |
+
- **Screen Readers:** ARIA labels for all interactive elements
|
| 106 |
+
- **Focus Indicators:** Visible focus states
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Provide specific design recommendations that developers can implement.
|
| 110 |
+
|
| 111 |
+
## Quality Enforcement
|
| 112 |
+
|
| 113 |
+
Before submitting, verify:
|
| 114 |
+
- [ ] Color palette has sufficient contrast ratios (4.5:1 minimum for normal text)
|
| 115 |
+
- [ ] All user flows have a defined start and end state
|
| 116 |
+
- [ ] Key screens cover all major functional requirements
|
| 117 |
+
- [ ] Component states (default, hover, active, disabled, error) are defined
|
| 118 |
+
- [ ] Accessibility requirements explicitly address keyboard navigation and screen readers
|
| 119 |
- [ ] Mobile responsive strategy is defined
|
|
|
app/routers/oracle.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
|
| 8 |
+
from app.core.llm_factory import get_chat_model
|
| 9 |
+
from app.core.prompt_loader import load_prompt
|
| 10 |
+
from app.core.schemas import TeamRole
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger("specs-before-code-api.oracle")
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/oracle", tags=["oracle"])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class OracleChatRequest(BaseModel):
|
| 18 |
+
message: str
|
| 19 |
+
history: list[dict[str, str]] = [] # [{"role":"user","content":"..."}]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class OracleChatResponse(BaseModel):
|
| 23 |
+
display_markdown: str
|
| 24 |
+
analysis: dict[str, Any]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _extract_json(content: str) -> dict[str, Any] | None:
|
| 28 |
+
import re
|
| 29 |
+
|
| 30 |
+
json_match = re.search(r"\{[\s\S]*\}", content)
|
| 31 |
+
if json_match:
|
| 32 |
+
try:
|
| 33 |
+
parsed = json.loads(json_match.group(0))
|
| 34 |
+
if isinstance(parsed, dict):
|
| 35 |
+
return parsed
|
| 36 |
+
except json.JSONDecodeError:
|
| 37 |
+
return None
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@router.post("/chat", response_model=OracleChatResponse)
|
| 42 |
+
async def oracle_chat(request: OracleChatRequest) -> OracleChatResponse:
|
| 43 |
+
"""Oracle advisory chat endpoint."""
|
| 44 |
+
prompt = load_prompt(TeamRole.ORACLE)
|
| 45 |
+
|
| 46 |
+
# Build conversation context
|
| 47 |
+
history_text = "\n".join(
|
| 48 |
+
[
|
| 49 |
+
f"{item.get('role', 'user')}: {item.get('content', '')}"
|
| 50 |
+
for item in request.history
|
| 51 |
+
]
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
user_context = (
|
| 55 |
+
f"{history_text}\nuser: {request.message}" if history_text else request.message
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
llm = get_chat_model(role=TeamRole.ORACLE, temperature=0.1, max_tokens=4096)
|
| 59 |
+
|
| 60 |
+
messages = [
|
| 61 |
+
{"role": "system", "content": prompt},
|
| 62 |
+
{"role": "user", "content": user_context},
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
response = await llm.ainvoke(messages)
|
| 66 |
+
content = (
|
| 67 |
+
response.content if isinstance(response.content, str) else str(response.content)
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
parsed = _extract_json(content)
|
| 71 |
+
if not parsed:
|
| 72 |
+
logger.warning("Oracle response was not valid JSON")
|
| 73 |
+
raise ValueError("Oracle response invalid JSON")
|
| 74 |
+
|
| 75 |
+
display_markdown = parsed.get("display_markdown", "")
|
| 76 |
+
analysis = parsed.get("analysis", {})
|
| 77 |
+
|
| 78 |
+
return OracleChatResponse(display_markdown=display_markdown, analysis=analysis)
|
app/routers/prd.py
ADDED
|
@@ -0,0 +1,966 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import uuid
|
| 4 |
+
from enum import Enum
|
| 5 |
+
from io import BytesIO
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
from fastapi import APIRouter, Depends, Query
|
| 10 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
from sqlalchemy.orm import Session
|
| 13 |
+
|
| 14 |
+
from app.core.agents import AgentSystem
|
| 15 |
+
from app.core.database import get_db
|
| 16 |
+
from app.core.llm_factory import get_chat_model
|
| 17 |
+
from app.core.models import PRDDocument
|
| 18 |
+
from app.core.schemas import ProjectRequest, TeamRole
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger("specs-before-code-api.prd")
|
| 21 |
+
|
| 22 |
+
# Create detailed PRD conversation logger
|
| 23 |
+
prd_detail_logger = logging.getLogger("prd_detail")
|
| 24 |
+
prd_detail_handler = logging.FileHandler(
|
| 25 |
+
Path(__file__).parent.parent.parent / "logs" / "prd_detail.log"
|
| 26 |
+
)
|
| 27 |
+
prd_detail_handler.setFormatter(logging.Formatter("%(asctime)s - %(message)s"))
|
| 28 |
+
prd_detail_logger.addHandler(prd_detail_handler)
|
| 29 |
+
prd_detail_logger.setLevel(logging.DEBUG)
|
| 30 |
+
prd_detail_logger.propagate = False
|
| 31 |
+
|
| 32 |
+
router = APIRouter(prefix="/prd", tags=["prd"])
|
| 33 |
+
|
| 34 |
+
PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
|
| 35 |
+
|
| 36 |
+
DEFAULT_MODEL = "meta/llama-3.1-70b-instruct"
|
| 37 |
+
MAX_FOLLOW_UPS = 2
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class PRDPhase(str, Enum):
|
| 41 |
+
EVALUATING = "evaluating"
|
| 42 |
+
COLLECTING = "collecting"
|
| 43 |
+
GENERATING = "generating"
|
| 44 |
+
COMPLETE = "complete"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class PRDStartRequest(BaseModel):
|
| 48 |
+
description: str
|
| 49 |
+
user_id: int # Required - enables proper ownership
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class PRDStartResponse(BaseModel):
|
| 53 |
+
session_id: str
|
| 54 |
+
message: str
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class PRDChatRequest(BaseModel):
|
| 58 |
+
session_id: str
|
| 59 |
+
message: str
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class PRDChatResponse(BaseModel):
|
| 63 |
+
agent_response: str
|
| 64 |
+
needs_more: bool
|
| 65 |
+
phase: str
|
| 66 |
+
missing_requirements: dict[str, bool]
|
| 67 |
+
questions: list[str]
|
| 68 |
+
generated_prd: str | None = None
|
| 69 |
+
judge_feedback: str | None = None # Include judge feedback for frontend
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class PRDDocumentResponse(BaseModel):
|
| 73 |
+
session_id: str
|
| 74 |
+
generated_prd: str
|
| 75 |
+
requirements_status: dict[str, bool]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class PRDStatusResponse(BaseModel):
|
| 79 |
+
session_id: str
|
| 80 |
+
phase: str
|
| 81 |
+
requirements_status: dict[str, bool]
|
| 82 |
+
collected_info: dict[str, str]
|
| 83 |
+
missing_sections: list[str]
|
| 84 |
+
follow_up_count: int
|
| 85 |
+
judge_approved: bool | None = None
|
| 86 |
+
judge_score: int | None = None
|
| 87 |
+
judge_feedback: str | None = None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class PRDSectionsResponse(BaseModel):
|
| 91 |
+
session_id: str
|
| 92 |
+
product_vision: str
|
| 93 |
+
key_features: dict[str, Any]
|
| 94 |
+
user_stories: list[dict[str, Any]]
|
| 95 |
+
assumptions: list[str]
|
| 96 |
+
full_text: str
|
| 97 |
+
judge_approved: bool
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class PipelineResponse(BaseModel):
|
| 101 |
+
pipeline_results: dict[str, Any]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _load_prompt(prompt_name: str) -> str:
|
| 105 |
+
"""Load a prompt from the prompts directory."""
|
| 106 |
+
path = PROMPTS_DIR / f"{prompt_name}.md"
|
| 107 |
+
if path.exists():
|
| 108 |
+
return path.read_text(encoding="utf-8")
|
| 109 |
+
raise FileNotFoundError(f"Prompt file not found: {path}")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _extract_json_from_response(content: str) -> dict[str, Any] | None:
|
| 113 |
+
"""Extract JSON from PRD response content."""
|
| 114 |
+
import re
|
| 115 |
+
|
| 116 |
+
# Try to find JSON block
|
| 117 |
+
json_match = re.search(r"```json\s*([\s\S]*?)\s*```", content)
|
| 118 |
+
if json_match:
|
| 119 |
+
try:
|
| 120 |
+
parsed = json.loads(json_match.group(1))
|
| 121 |
+
if isinstance(parsed, dict):
|
| 122 |
+
return {str(key): value for key, value in parsed.items()}
|
| 123 |
+
except json.JSONDecodeError:
|
| 124 |
+
pass
|
| 125 |
+
|
| 126 |
+
# Try to find raw JSON object
|
| 127 |
+
json_match = re.search(r"\{[\s\S]*\}", content)
|
| 128 |
+
if json_match:
|
| 129 |
+
try:
|
| 130 |
+
parsed = json.loads(json_match.group(0))
|
| 131 |
+
if isinstance(parsed, dict):
|
| 132 |
+
return {str(key): value for key, value in parsed.items()}
|
| 133 |
+
except json.JSONDecodeError:
|
| 134 |
+
pass
|
| 135 |
+
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _normalize_llm_content(content: str | list[str | dict[str, Any]]) -> str:
|
| 140 |
+
"""Normalize LLM content into a string."""
|
| 141 |
+
if isinstance(content, str):
|
| 142 |
+
return content
|
| 143 |
+
|
| 144 |
+
parts: list[str] = []
|
| 145 |
+
for item in content:
|
| 146 |
+
if isinstance(item, str):
|
| 147 |
+
parts.append(item)
|
| 148 |
+
continue
|
| 149 |
+
if isinstance(item, dict):
|
| 150 |
+
text = item.get("text")
|
| 151 |
+
if isinstance(text, str):
|
| 152 |
+
parts.append(text)
|
| 153 |
+
continue
|
| 154 |
+
try:
|
| 155 |
+
parts.append(json.dumps(item))
|
| 156 |
+
except TypeError:
|
| 157 |
+
parts.append(str(item))
|
| 158 |
+
return "\n".join(part for part in parts if part)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _normalize_assumptions(value: str | list[str] | None) -> list[str]:
|
| 162 |
+
"""Normalize assumptions stored as string or list into list[str]."""
|
| 163 |
+
if value is None:
|
| 164 |
+
return []
|
| 165 |
+
if isinstance(value, list):
|
| 166 |
+
return [str(item).strip() for item in value if str(item).strip()]
|
| 167 |
+
if isinstance(value, str):
|
| 168 |
+
if not value.strip():
|
| 169 |
+
return []
|
| 170 |
+
try:
|
| 171 |
+
parsed = json.loads(value)
|
| 172 |
+
if isinstance(parsed, list):
|
| 173 |
+
return [str(item).strip() for item in parsed if str(item).strip()]
|
| 174 |
+
except json.JSONDecodeError:
|
| 175 |
+
pass
|
| 176 |
+
return [line.strip() for line in value.splitlines() if line.strip()]
|
| 177 |
+
return []
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _parse_prd_sections(prd_content: str) -> dict[str, Any]:
|
| 181 |
+
"""Parse PRD content and extract structured sections from markdown or JSON."""
|
| 182 |
+
import re
|
| 183 |
+
|
| 184 |
+
# First try JSON parsing
|
| 185 |
+
parsed = _extract_json_from_response(prd_content)
|
| 186 |
+
|
| 187 |
+
if parsed:
|
| 188 |
+
return {
|
| 189 |
+
"product_vision": parsed.get("product_vision", ""),
|
| 190 |
+
"key_features": parsed.get("features", {}),
|
| 191 |
+
"user_stories": parsed.get("user_stories", []),
|
| 192 |
+
"assumptions": parsed.get("assumptions", []),
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
# Parse markdown format - extract sections by headers
|
| 196 |
+
sections = {}
|
| 197 |
+
|
| 198 |
+
# Extract Product Vision
|
| 199 |
+
vision_match = re.search(
|
| 200 |
+
r"##?\s*1\.?\s*Product Vision\s*\n(.*?)(?=\n##|\Z)",
|
| 201 |
+
prd_content,
|
| 202 |
+
re.DOTALL | re.IGNORECASE,
|
| 203 |
+
)
|
| 204 |
+
if not vision_match:
|
| 205 |
+
vision_match = re.search(
|
| 206 |
+
r"Product Vision\s*\n(.*?)(?=\n##|\Z)",
|
| 207 |
+
prd_content,
|
| 208 |
+
re.DOTALL | re.IGNORECASE,
|
| 209 |
+
)
|
| 210 |
+
sections["product_vision"] = vision_match.group(1).strip() if vision_match else ""
|
| 211 |
+
|
| 212 |
+
# Extract Target Users
|
| 213 |
+
users_match = re.search(
|
| 214 |
+
r"##?\s*Target Users?\s*\n(.*?)(?=\n##|\Z)",
|
| 215 |
+
prd_content,
|
| 216 |
+
re.DOTALL | re.IGNORECASE,
|
| 217 |
+
)
|
| 218 |
+
sections["target_users"] = users_match.group(1).strip() if users_match else ""
|
| 219 |
+
|
| 220 |
+
# Extract Key Features
|
| 221 |
+
features_match = re.search(
|
| 222 |
+
r"##?\s*3\.?\s*Key Features?\s*\n(.*?)(?=\n##|\Z)",
|
| 223 |
+
prd_content,
|
| 224 |
+
re.DOTALL | re.IGNORECASE,
|
| 225 |
+
)
|
| 226 |
+
if not features_match:
|
| 227 |
+
features_match = re.search(
|
| 228 |
+
r"Key Features?\s*\n(.*?)(?=\n##|\Z)",
|
| 229 |
+
prd_content,
|
| 230 |
+
re.DOTALL | re.IGNORECASE,
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Parse feature list from markdown
|
| 234 |
+
features = {}
|
| 235 |
+
if features_match:
|
| 236 |
+
feature_text = features_match.group(1)
|
| 237 |
+
# Find all feature titles (### F1: or ### Feature Name)
|
| 238 |
+
feature_matches = re.findall(r"###\s*(?:F\d+:?\s*)?([^\n]+)", feature_text)
|
| 239 |
+
for i, title in enumerate(feature_matches, 1):
|
| 240 |
+
# Try to find priority
|
| 241 |
+
priority = "should"
|
| 242 |
+
priority_match = re.search(
|
| 243 |
+
r"priority[:\s]*(\w+)", feature_text, re.IGNORECASE
|
| 244 |
+
)
|
| 245 |
+
if priority_match:
|
| 246 |
+
p = priority_match.group(1).lower()
|
| 247 |
+
if "must" in p:
|
| 248 |
+
priority = "must"
|
| 249 |
+
elif "could" in p:
|
| 250 |
+
priority = "could"
|
| 251 |
+
features[f"F{i}"] = {"title": title.strip(), "priority": priority}
|
| 252 |
+
|
| 253 |
+
sections["key_features"] = features
|
| 254 |
+
|
| 255 |
+
# Extract User Stories - look for table format
|
| 256 |
+
stories_match = re.search(
|
| 257 |
+
r"##?\s*4\.?\s*User Stories?\s*\n(.*?)(?=\n##|\Z)",
|
| 258 |
+
prd_content,
|
| 259 |
+
re.DOTALL | re.IGNORECASE,
|
| 260 |
+
)
|
| 261 |
+
user_stories = []
|
| 262 |
+
if stories_match:
|
| 263 |
+
table_text = stories_match.group(1)
|
| 264 |
+
# Parse markdown table rows
|
| 265 |
+
rows = re.findall(r"\|\s*([^|]+)\s*\|", table_text)
|
| 266 |
+
# Skip header row if present, extract story info
|
| 267 |
+
for row in rows:
|
| 268 |
+
if "---" in row or "ID" in row:
|
| 269 |
+
continue
|
| 270 |
+
user_stories.append({"content": row.strip()})
|
| 271 |
+
|
| 272 |
+
sections["user_stories"] = user_stories
|
| 273 |
+
|
| 274 |
+
# Extract Assumptions
|
| 275 |
+
assumptions_match = re.search(
|
| 276 |
+
r"##?\s*5\.?\s*Assumptions.*?\n(.*?)(?=\n##|\Z)",
|
| 277 |
+
prd_content,
|
| 278 |
+
re.DOTALL | re.IGNORECASE,
|
| 279 |
+
)
|
| 280 |
+
if not assumptions_match:
|
| 281 |
+
assumptions_match = re.search(
|
| 282 |
+
r"Assumptions.*?\n(.*?)(?=\n##|\Z)", prd_content, re.DOTALL | re.IGNORECASE
|
| 283 |
+
)
|
| 284 |
+
assumptions = []
|
| 285 |
+
if assumptions_match:
|
| 286 |
+
# Extract bullet points
|
| 287 |
+
items = re.findall(
|
| 288 |
+
r"[-*]\s*(.+?)(?=\n[-*]|\n\n|\Z)", assumptions_match.group(1)
|
| 289 |
+
)
|
| 290 |
+
assumptions = [item.strip() for item in items]
|
| 291 |
+
sections["assumptions"] = assumptions
|
| 292 |
+
|
| 293 |
+
return {
|
| 294 |
+
"product_vision": sections.get("product_vision", ""),
|
| 295 |
+
"key_features": sections.get("key_features", {}),
|
| 296 |
+
"user_stories": sections.get("user_stories", []),
|
| 297 |
+
"assumptions": sections.get("assumptions", []),
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
async def _evaluate_requirements(
|
| 302 |
+
user_message: str, collected_info: dict[str, str]
|
| 303 |
+
) -> dict[str, bool]:
|
| 304 |
+
"""
|
| 305 |
+
LLM-based evaluation of requirements.
|
| 306 |
+
Uses the LLM to intelligently determine which PRD sections are satisfied
|
| 307 |
+
based on the user's input, handling natural language and vague descriptions.
|
| 308 |
+
"""
|
| 309 |
+
prd_detail_logger.info("=== EVALUATE REQUIREMENTS ===")
|
| 310 |
+
prd_detail_logger.info(f"user_message: {user_message[:300]}...")
|
| 311 |
+
prd_detail_logger.info(f"collected_info keys: {list(collected_info.keys())}")
|
| 312 |
+
|
| 313 |
+
# First, do a quick keyword pass for obvious cases (fast path)
|
| 314 |
+
combined_text = f"{user_message} {' '.join(collected_info.values())}".lower()
|
| 315 |
+
|
| 316 |
+
quick_requirements = {
|
| 317 |
+
"vision": False,
|
| 318 |
+
"features": False,
|
| 319 |
+
"user_stories": False,
|
| 320 |
+
"acceptance_criteria": False,
|
| 321 |
+
"assumptions": False,
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
# Quick obvious keyword matches - but still use LLM for nuanced evaluation
|
| 325 |
+
# We only use this for known obvious cases, then let LLM make the final decision
|
| 326 |
+
if any(
|
| 327 |
+
w in combined_text for w in ["problem", "solve for", "target users", "for whom"]
|
| 328 |
+
):
|
| 329 |
+
quick_requirements["vision"] = True
|
| 330 |
+
if any(w in combined_text for w in ["feature", "key feature", "functionality"]):
|
| 331 |
+
quick_requirements["features"] = True
|
| 332 |
+
if any(
|
| 333 |
+
w in combined_text
|
| 334 |
+
for w in ["timeline", "deadline", "launch date", "completion"]
|
| 335 |
+
):
|
| 336 |
+
quick_requirements["acceptance_criteria"] = True
|
| 337 |
+
if any(
|
| 338 |
+
w in combined_text
|
| 339 |
+
for w in ["constraint", "limitation", "budget", "team size", "scope"]
|
| 340 |
+
):
|
| 341 |
+
quick_requirements["assumptions"] = True
|
| 342 |
+
|
| 343 |
+
prd_detail_logger.info(f"Quick requirements (keyword match): {quick_requirements}")
|
| 344 |
+
|
| 345 |
+
# Always use LLM for evaluation - it's better at understanding context
|
| 346 |
+
# The quick_requirements just helps ensure we don't miss obvious things if LLM fails
|
| 347 |
+
|
| 348 |
+
# Use LLM for nuanced evaluation
|
| 349 |
+
try:
|
| 350 |
+
context = "\n\n".join([f"{k}: {v}" for k, v in collected_info.items()])
|
| 351 |
+
if context:
|
| 352 |
+
context = (
|
| 353 |
+
f"Previous context:\n{context}\n\nCurrent user input:\n{user_message}"
|
| 354 |
+
)
|
| 355 |
+
else:
|
| 356 |
+
context = user_message
|
| 357 |
+
|
| 358 |
+
prd_detail_logger.info(
|
| 359 |
+
f"Context sent to LLM (first 500 chars): {context[:500]}..."
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
evaluation_prompt = f"""You are a PRD Requirements Analyzer. Your job is to evaluate whether we have ENOUGH information to generate a complete PRD.
|
| 363 |
+
|
| 364 |
+
CRITICAL: You must evaluate the ENTIRE conversation context provided below, not just the most recent message.
|
| 365 |
+
|
| 366 |
+
For each requirement, consider ALL information provided in the entire context:
|
| 367 |
+
- Information from the INITIAL product description
|
| 368 |
+
- Information from the user's answers to follow-up questions
|
| 369 |
+
- Any additional details the user has shared
|
| 370 |
+
|
| 371 |
+
Evaluate each requirement and return a JSON object with boolean values:
|
| 372 |
+
- "vision": true if we know WHO the product is for and WHAT problem it solves
|
| 373 |
+
- "features": true if we know the KEY capabilities or features
|
| 374 |
+
- "user_stories": true if we know WHO the users are and WHAT they need to accomplish (formal "As a..." format NOT required - knowing WHO the users are is enough)
|
| 375 |
+
- "acceptance_criteria": true if we know how success will be measured, timelines, or completion conditions
|
| 376 |
+
- "assumptions": true if we know any constraints, dependencies, or scope limitations
|
| 377 |
+
|
| 378 |
+
Be flexible - the user may describe things in natural language without using specific keywords.
|
| 379 |
+
For example:
|
| 380 |
+
- "TARGET USERS: students" → user_stories = true (we know who users are)
|
| 381 |
+
- "timeline is 12 weeks" → acceptance_criteria = true (we have a completion condition)
|
| 382 |
+
- "first version in 3 months" → acceptance_criteria = true
|
| 383 |
+
- "Target Timeline: 3-month MVP" → acceptance_criteria = true (timeline mentioned)
|
| 384 |
+
- "Constraints: small team, limited budget" → assumptions = true (constraints mentioned)
|
| 385 |
+
- "MVP timeline, standard web platform" → both acceptance_criteria AND assumptions = true
|
| 386 |
+
- If the initial description already mentions target users, that's enough for user_stories
|
| 387 |
+
|
| 388 |
+
IMPORTANT: If ANY part of the context mentions users, goals, timeline, features, etc., mark that requirement as satisfied.
|
| 389 |
+
|
| 390 |
+
Return ONLY a JSON object, no other text.
|
| 391 |
+
Example: {{"vision": true, "features": true, "user_stories": false, "acceptance_criteria": true, "assumptions": false}}
|
| 392 |
+
|
| 393 |
+
Full conversation context:
|
| 394 |
+
{context}"""
|
| 395 |
+
|
| 396 |
+
llm = get_chat_model(
|
| 397 |
+
role=TeamRole.PRODUCT_OWNER,
|
| 398 |
+
model=DEFAULT_MODEL,
|
| 399 |
+
temperature=0.1,
|
| 400 |
+
max_tokens=256,
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
response = await llm.ainvoke([{"role": "user", "content": evaluation_prompt}])
|
| 404 |
+
content = _normalize_llm_content(response.content).strip()
|
| 405 |
+
|
| 406 |
+
# Try to parse JSON from response
|
| 407 |
+
import re
|
| 408 |
+
|
| 409 |
+
json_match = re.search(r"\{[^}]+\}", content)
|
| 410 |
+
if json_match:
|
| 411 |
+
result = json.loads(json_match.group())
|
| 412 |
+
if not isinstance(result, dict):
|
| 413 |
+
prd_detail_logger.warning(f"LLM returned non-dict: {result}")
|
| 414 |
+
return quick_requirements
|
| 415 |
+
prd_detail_logger.info(f"LLM raw result: {result}")
|
| 416 |
+
# Merge with quick requirements (LLM takes precedence)
|
| 417 |
+
for key in quick_requirements:
|
| 418 |
+
if key in result:
|
| 419 |
+
quick_requirements[key] = bool(result[key])
|
| 420 |
+
prd_detail_logger.info(
|
| 421 |
+
f"Final requirements after merge: {quick_requirements}"
|
| 422 |
+
)
|
| 423 |
+
logger.debug("LLM evaluation result: %s", quick_requirements)
|
| 424 |
+
return quick_requirements
|
| 425 |
+
else:
|
| 426 |
+
prd_detail_logger.warning(
|
| 427 |
+
f"Could not parse JSON from LLM response: {content[:200]}"
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
except Exception as e:
|
| 431 |
+
logger.warning(f"LLM evaluation failed, falling back to keywords: {e}")
|
| 432 |
+
prd_detail_logger.error(f"LLM evaluation exception: {str(e)}")
|
| 433 |
+
|
| 434 |
+
# Fallback to quick requirements
|
| 435 |
+
prd_detail_logger.info(f"Falling back to quick requirements: {quick_requirements}")
|
| 436 |
+
return quick_requirements
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def _determine_questions(missing: dict[str, bool]) -> list[str]:
|
| 440 |
+
"""Generate targeted questions for missing requirements."""
|
| 441 |
+
questions_map = {
|
| 442 |
+
"vision": "What problem does your product solve, and who is it for?",
|
| 443 |
+
"features": "What are the key features or capabilities your product should have?",
|
| 444 |
+
"user_stories": "Who are your users and what do they need to accomplish with this product?",
|
| 445 |
+
"acceptance_criteria": "How will you know if a feature works correctly? What would success look like?",
|
| 446 |
+
"assumptions": "Are there any constraints, timelines, or dependencies we should know about?",
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
questions = []
|
| 450 |
+
for section, is_missing in missing.items():
|
| 451 |
+
if is_missing and section in questions_map:
|
| 452 |
+
questions.append(questions_map[section])
|
| 453 |
+
|
| 454 |
+
return questions[:2]
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def _generate_intro_message(description: str, requirements: dict[str, bool]) -> str:
|
| 458 |
+
"""
|
| 459 |
+
Generate a contextual first message based on what's already provided.
|
| 460 |
+
"""
|
| 461 |
+
missing = [k for k, v in requirements.items() if not v]
|
| 462 |
+
|
| 463 |
+
if not missing:
|
| 464 |
+
return "Thank you for the detailed information. I have enough to generate the PRD now. One moment please..."
|
| 465 |
+
|
| 466 |
+
if len(missing) >= 4:
|
| 467 |
+
intro = "Thank you for sharing your project idea. I can see you've provided some initial context."
|
| 468 |
+
elif len(missing) >= 2:
|
| 469 |
+
intro = "Thank you for that information. I have a good understanding of your product."
|
| 470 |
+
else:
|
| 471 |
+
intro = "I appreciate you sharing those details."
|
| 472 |
+
|
| 473 |
+
questions = []
|
| 474 |
+
|
| 475 |
+
if "vision" in missing:
|
| 476 |
+
questions.append("What problem does your product solve, and who is it for?")
|
| 477 |
+
|
| 478 |
+
if "features" in missing:
|
| 479 |
+
questions.append(
|
| 480 |
+
"What are the key features or capabilities your product should have?"
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
if "user_stories" in missing:
|
| 484 |
+
questions.append(
|
| 485 |
+
"Who are your users and what do they need to accomplish with this product?"
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
if "acceptance_criteria" in missing:
|
| 489 |
+
questions.append(
|
| 490 |
+
"How will you know if a feature works correctly? What would success look like?"
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
if "assumptions" in missing:
|
| 494 |
+
questions.append(
|
| 495 |
+
"Are there any constraints, timelines, or dependencies we should know about?"
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
question_text = "\n".join([f"- {q}" for q in questions[:2]])
|
| 499 |
+
|
| 500 |
+
return f"{intro}\n\nTo create a comprehensive PRD, I need a bit more detail:\n\n{question_text}\n\nPlease share any additional details you're comfortable with."
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
@router.post("/start", response_model=PRDStartResponse)
|
| 504 |
+
async def start_prd_session(
|
| 505 |
+
request: PRDStartRequest, db: Session = Depends(get_db)
|
| 506 |
+
) -> PRDStartResponse:
|
| 507 |
+
"""Initialize a new PRD session with the user's initial description."""
|
| 508 |
+
session_id = str(uuid.uuid4())
|
| 509 |
+
|
| 510 |
+
initial_requirements = await _evaluate_requirements(request.description, {})
|
| 511 |
+
|
| 512 |
+
missing = {k: not v for k, v in initial_requirements.items()}
|
| 513 |
+
# If we ask follow-up questions at start, treat that as the single allowed follow-up.
|
| 514 |
+
initial_follow_up_count = 1 if any(missing.values()) else 0
|
| 515 |
+
|
| 516 |
+
prd_doc = PRDDocument(
|
| 517 |
+
session_id=session_id,
|
| 518 |
+
user_id=request.user_id,
|
| 519 |
+
initial_description=request.description,
|
| 520 |
+
requirements_status=initial_requirements,
|
| 521 |
+
collected_info={"initial": request.description},
|
| 522 |
+
phase=PRDPhase.EVALUATING.value,
|
| 523 |
+
follow_up_count=initial_follow_up_count,
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
db.add(prd_doc)
|
| 527 |
+
db.commit()
|
| 528 |
+
|
| 529 |
+
first_message = _generate_intro_message(request.description, initial_requirements)
|
| 530 |
+
|
| 531 |
+
return PRDStartResponse(
|
| 532 |
+
session_id=session_id,
|
| 533 |
+
message=first_message,
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
@router.post("/chat", response_model=PRDChatResponse)
|
| 538 |
+
async def prd_chat(
|
| 539 |
+
request: PRDChatRequest, db: Session = Depends(get_db)
|
| 540 |
+
) -> PRDChatResponse:
|
| 541 |
+
"""Main PRD chat endpoint - evaluates input, asks follow-ups, or generates PRD."""
|
| 542 |
+
session_id = request.session_id
|
| 543 |
+
prd_doc = db.query(PRDDocument).filter_by(session_id=session_id).first()
|
| 544 |
+
|
| 545 |
+
prd_detail_logger.info(f"=== PRD CHAT START === session={session_id[:8]}...")
|
| 546 |
+
prd_detail_logger.info(f"Current phase: {prd_doc.phase if prd_doc else 'NONE'}")
|
| 547 |
+
prd_detail_logger.info(
|
| 548 |
+
f"Current follow_up_count: {prd_doc.follow_up_count if prd_doc else 0}"
|
| 549 |
+
)
|
| 550 |
+
prd_detail_logger.info(
|
| 551 |
+
f"Current requirements_status: {prd_doc.requirements_status if prd_doc else {}}"
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
if not prd_doc:
|
| 555 |
+
prd_detail_logger.error(f"Session not found: {session_id}")
|
| 556 |
+
return PRDChatResponse(
|
| 557 |
+
agent_response="Session not found. Please start a new PRD session.",
|
| 558 |
+
needs_more=False,
|
| 559 |
+
phase="error",
|
| 560 |
+
missing_requirements={},
|
| 561 |
+
questions=[],
|
| 562 |
+
generated_prd=None,
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
if prd_doc.phase == PRDPhase.COMPLETE.value:
|
| 566 |
+
prd_detail_logger.info(f"PRD already complete for session {session_id[:8]}")
|
| 567 |
+
return PRDChatResponse(
|
| 568 |
+
agent_response="This PRD is already complete. You can download it or send it to the pipeline.",
|
| 569 |
+
needs_more=False,
|
| 570 |
+
phase=prd_doc.phase,
|
| 571 |
+
missing_requirements=prd_doc.requirements_status,
|
| 572 |
+
questions=[],
|
| 573 |
+
generated_prd=prd_doc.generated_prd,
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
current_collected = dict(prd_doc.collected_info)
|
| 577 |
+
current_collected[f"message_{prd_doc.follow_up_count}"] = request.message
|
| 578 |
+
|
| 579 |
+
prd_detail_logger.info(f"User message: {request.message[:200]}...")
|
| 580 |
+
prd_detail_logger.info(f"Collected info keys: {list(current_collected.keys())}")
|
| 581 |
+
|
| 582 |
+
new_requirements = await _evaluate_requirements(request.message, current_collected)
|
| 583 |
+
|
| 584 |
+
prd_detail_logger.info(f"LLM Evaluation result: {new_requirements}")
|
| 585 |
+
|
| 586 |
+
logger.debug(
|
| 587 |
+
"PRD requirements evaluation: user_message=%s, new_requirements=%s",
|
| 588 |
+
request.message[:100],
|
| 589 |
+
new_requirements,
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
updated_requirements = dict(prd_doc.requirements_status)
|
| 593 |
+
for key, value in new_requirements.items():
|
| 594 |
+
if value:
|
| 595 |
+
updated_requirements[key] = True
|
| 596 |
+
|
| 597 |
+
missing = {k: not v for k, v in updated_requirements.items()}
|
| 598 |
+
|
| 599 |
+
prd_detail_logger.info(f"Missing requirements: {missing}")
|
| 600 |
+
prd_detail_logger.info(
|
| 601 |
+
f"Stored requirements (before update): {prd_doc.requirements_status}"
|
| 602 |
+
)
|
| 603 |
+
prd_detail_logger.info(f"Updated requirements: {updated_requirements}")
|
| 604 |
+
|
| 605 |
+
logger.debug(
|
| 606 |
+
"PRD requirements: stored=%s, new=%s, updated=%s, missing=%s",
|
| 607 |
+
prd_doc.requirements_status,
|
| 608 |
+
new_requirements,
|
| 609 |
+
updated_requirements,
|
| 610 |
+
missing,
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
needs_more = any(missing.values())
|
| 614 |
+
questions = _determine_questions(missing) if needs_more else []
|
| 615 |
+
|
| 616 |
+
prd_detail_logger.info(f"needs_more={needs_more}, questions={questions}")
|
| 617 |
+
|
| 618 |
+
logger.debug(
|
| 619 |
+
"PRD chat: follow_up=%d, needs_more=%s, questions=%s",
|
| 620 |
+
prd_doc.follow_up_count,
|
| 621 |
+
needs_more,
|
| 622 |
+
questions,
|
| 623 |
+
)
|
| 624 |
+
|
| 625 |
+
prd_doc.collected_info = current_collected
|
| 626 |
+
prd_doc.requirements_status = updated_requirements
|
| 627 |
+
|
| 628 |
+
if prd_doc.follow_up_count >= MAX_FOLLOW_UPS - 1:
|
| 629 |
+
prd_detail_logger.info(
|
| 630 |
+
f"Max follow-ups reached ({MAX_FOLLOW_UPS}), forcing generation"
|
| 631 |
+
)
|
| 632 |
+
needs_more = False
|
| 633 |
+
questions = []
|
| 634 |
+
|
| 635 |
+
if not needs_more or prd_doc.follow_up_count >= MAX_FOLLOW_UPS - 1:
|
| 636 |
+
prd_detail_logger.info("=== ENTERING GENERATION PHASE ===")
|
| 637 |
+
prd_doc.phase = PRDPhase.GENERATING.value
|
| 638 |
+
db.commit()
|
| 639 |
+
|
| 640 |
+
try:
|
| 641 |
+
product_owner_prompt = _load_prompt("product_owner")
|
| 642 |
+
|
| 643 |
+
collected_context = "\n\n".join(
|
| 644 |
+
[f"{k}: {v}" for k, v in current_collected.items()]
|
| 645 |
+
)
|
| 646 |
+
|
| 647 |
+
prd_detail_logger.info(
|
| 648 |
+
f"Generating PRD with context length: {len(collected_context)} chars"
|
| 649 |
+
)
|
| 650 |
+
|
| 651 |
+
product_owner_llm = get_chat_model(
|
| 652 |
+
role=TeamRole.PRODUCT_OWNER,
|
| 653 |
+
model=DEFAULT_MODEL,
|
| 654 |
+
temperature=0.5,
|
| 655 |
+
max_tokens=4096,
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
product_owner_messages = [
|
| 659 |
+
{"role": "system", "content": product_owner_prompt},
|
| 660 |
+
{"role": "user", "content": f"Project Context:\n{collected_context}"},
|
| 661 |
+
]
|
| 662 |
+
|
| 663 |
+
prd_response = await product_owner_llm.ainvoke(product_owner_messages)
|
| 664 |
+
|
| 665 |
+
prd_content = _normalize_llm_content(prd_response.content)
|
| 666 |
+
|
| 667 |
+
prd_detail_logger.info(f"PRD generated, length: {len(prd_content)} chars")
|
| 668 |
+
|
| 669 |
+
# Parse structured PRD sections
|
| 670 |
+
parsed_sections = _parse_prd_sections(prd_content)
|
| 671 |
+
prd_doc.product_vision = parsed_sections.get("product_vision", "")
|
| 672 |
+
prd_doc.key_features = parsed_sections.get("key_features", {})
|
| 673 |
+
prd_doc.user_stories = parsed_sections.get("user_stories", [])
|
| 674 |
+
prd_doc.assumptions = parsed_sections.get("assumptions", [])
|
| 675 |
+
|
| 676 |
+
prd_detail_logger.info("=== RUNNING JUDGE EVALUATION ===")
|
| 677 |
+
|
| 678 |
+
# Run judge evaluation
|
| 679 |
+
agent_system = AgentSystem()
|
| 680 |
+
judge_output = await agent_system.evaluate_step(
|
| 681 |
+
role=TeamRole.PRODUCT_OWNER,
|
| 682 |
+
content=prd_content,
|
| 683 |
+
context=collected_context,
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
+
prd_detail_logger.info(
|
| 687 |
+
f"Judge result: is_approved={judge_output.is_approved}, score={judge_output.score}"
|
| 688 |
+
)
|
| 689 |
+
prd_detail_logger.info(f"Judge feedback: {judge_output.feedback}")
|
| 690 |
+
|
| 691 |
+
# Store judge results
|
| 692 |
+
prd_doc.judge_score = judge_output.score
|
| 693 |
+
prd_doc.judge_approved = judge_output.is_approved
|
| 694 |
+
prd_doc.judge_feedback = judge_output.feedback
|
| 695 |
+
|
| 696 |
+
# Always complete after first follow-up to avoid user frustration.
|
| 697 |
+
# We still store judge feedback internally for diagnostics.
|
| 698 |
+
if not judge_output.is_approved:
|
| 699 |
+
prd_detail_logger.info(
|
| 700 |
+
"Judge rejected PRD, but auto-completing to avoid extra follow-ups"
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
prd_detail_logger.info("=== PRD AUTO-COMPLETE ===")
|
| 704 |
+
prd_doc.phase = PRDPhase.COMPLETE.value
|
| 705 |
+
prd_doc.judge_approved = True
|
| 706 |
+
response_message = prd_content
|
| 707 |
+
|
| 708 |
+
db.commit()
|
| 709 |
+
|
| 710 |
+
# Return clean response - always complete
|
| 711 |
+
is_complete = True
|
| 712 |
+
questions_list: list[str] = []
|
| 713 |
+
|
| 714 |
+
prd_detail_logger.info("=== RETURNING RESPONSE ===")
|
| 715 |
+
prd_detail_logger.info(
|
| 716 |
+
f"phase={prd_doc.phase}, is_complete={is_complete}, needs_more={not is_complete}"
|
| 717 |
+
)
|
| 718 |
+
prd_detail_logger.info(f"questions={questions_list}")
|
| 719 |
+
prd_detail_logger.info(f"judge_approved in db={prd_doc.judge_approved}")
|
| 720 |
+
|
| 721 |
+
return PRDChatResponse(
|
| 722 |
+
agent_response=response_message,
|
| 723 |
+
needs_more=not is_complete,
|
| 724 |
+
phase=prd_doc.phase,
|
| 725 |
+
missing_requirements=updated_requirements,
|
| 726 |
+
questions=questions_list,
|
| 727 |
+
generated_prd=prd_content, # Always return PRD to user
|
| 728 |
+
judge_feedback=None,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
except Exception as e:
|
| 732 |
+
prd_detail_logger.error(f"Error during PRD generation: {str(e)}")
|
| 733 |
+
prd_doc.phase = PRDPhase.EVALUATING.value
|
| 734 |
+
db.commit()
|
| 735 |
+
return PRDChatResponse(
|
| 736 |
+
agent_response=f"Error generating PRD: {str(e)}",
|
| 737 |
+
needs_more=True,
|
| 738 |
+
phase=PRDPhase.EVALUATING.value,
|
| 739 |
+
missing_requirements=updated_requirements,
|
| 740 |
+
questions=["Could you provide more details about your product?"],
|
| 741 |
+
generated_prd=None,
|
| 742 |
+
judge_feedback=None,
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
prd_detail_logger.info("=== COLLECTING PHASE - ASKING QUESTIONS ===")
|
| 746 |
+
prd_doc.follow_up_count += 1
|
| 747 |
+
prd_doc.phase = PRDPhase.COLLECTING.value
|
| 748 |
+
db.commit()
|
| 749 |
+
|
| 750 |
+
question_text = "\n".join([f"- {q}" for q in questions]) if questions else ""
|
| 751 |
+
|
| 752 |
+
agent_response = f"Thank you for that information. To create a comprehensive PRD, I need a bit more detail:\n\n{question_text}\n\nPlease share any additional details you're comfortable with."
|
| 753 |
+
|
| 754 |
+
if not questions:
|
| 755 |
+
agent_response = "Thank you for the additional information. I have enough to generate the PRD now."
|
| 756 |
+
|
| 757 |
+
prd_detail_logger.info(f"Returning questions: {questions}")
|
| 758 |
+
|
| 759 |
+
return PRDChatResponse(
|
| 760 |
+
agent_response=agent_response,
|
| 761 |
+
needs_more=bool(questions),
|
| 762 |
+
phase=prd_doc.phase,
|
| 763 |
+
missing_requirements=updated_requirements,
|
| 764 |
+
questions=questions,
|
| 765 |
+
generated_prd=None,
|
| 766 |
+
)
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
@router.get("/doc/{session_id}", response_model=PRDDocumentResponse)
|
| 770 |
+
def get_prd_doc(session_id: str, db: Session = Depends(get_db)) -> PRDDocumentResponse:
|
| 771 |
+
"""Get the generated PRD document."""
|
| 772 |
+
prd_doc = db.query(PRDDocument).filter_by(session_id=session_id).first()
|
| 773 |
+
|
| 774 |
+
if not prd_doc:
|
| 775 |
+
return PRDDocumentResponse(
|
| 776 |
+
session_id=session_id,
|
| 777 |
+
generated_prd="",
|
| 778 |
+
requirements_status={},
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
return PRDDocumentResponse(
|
| 782 |
+
session_id=session_id,
|
| 783 |
+
generated_prd=prd_doc.generated_prd or "PRD not yet generated",
|
| 784 |
+
requirements_status=prd_doc.requirements_status,
|
| 785 |
+
)
|
| 786 |
+
|
| 787 |
+
|
| 788 |
+
@router.get("/status/{session_id}", response_model=PRDStatusResponse)
|
| 789 |
+
def get_prd_status(session_id: str, db: Session = Depends(get_db)) -> PRDStatusResponse:
|
| 790 |
+
"""Get the current status of a PRD session."""
|
| 791 |
+
prd_doc = db.query(PRDDocument).filter_by(session_id=session_id).first()
|
| 792 |
+
|
| 793 |
+
if not prd_doc:
|
| 794 |
+
return PRDStatusResponse(
|
| 795 |
+
session_id=session_id,
|
| 796 |
+
phase="not_found",
|
| 797 |
+
requirements_status={},
|
| 798 |
+
collected_info={},
|
| 799 |
+
missing_sections=[],
|
| 800 |
+
follow_up_count=0,
|
| 801 |
+
)
|
| 802 |
+
|
| 803 |
+
missing = [k for k, v in prd_doc.requirements_status.items() if not v]
|
| 804 |
+
|
| 805 |
+
return PRDStatusResponse(
|
| 806 |
+
session_id=session_id,
|
| 807 |
+
phase=prd_doc.phase,
|
| 808 |
+
requirements_status=prd_doc.requirements_status,
|
| 809 |
+
collected_info=prd_doc.collected_info,
|
| 810 |
+
missing_sections=missing,
|
| 811 |
+
follow_up_count=prd_doc.follow_up_count,
|
| 812 |
+
judge_approved=prd_doc.judge_approved,
|
| 813 |
+
judge_score=prd_doc.judge_score,
|
| 814 |
+
judge_feedback=prd_doc.judge_feedback,
|
| 815 |
+
)
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
@router.get("/sections/{session_id}", response_model=PRDSectionsResponse)
|
| 819 |
+
def get_prd_sections(
|
| 820 |
+
session_id: str, db: Session = Depends(get_db)
|
| 821 |
+
) -> PRDSectionsResponse:
|
| 822 |
+
"""Get structured PRD sections for SRS pipeline."""
|
| 823 |
+
prd_doc = db.query(PRDDocument).filter_by(session_id=session_id).first()
|
| 824 |
+
|
| 825 |
+
if not prd_doc or not prd_doc.generated_prd:
|
| 826 |
+
return PRDSectionsResponse(
|
| 827 |
+
session_id=session_id,
|
| 828 |
+
product_vision="",
|
| 829 |
+
key_features={},
|
| 830 |
+
user_stories=[],
|
| 831 |
+
assumptions=[],
|
| 832 |
+
full_text="",
|
| 833 |
+
judge_approved=False,
|
| 834 |
+
)
|
| 835 |
+
|
| 836 |
+
return PRDSectionsResponse(
|
| 837 |
+
session_id=session_id,
|
| 838 |
+
product_vision=prd_doc.product_vision or "",
|
| 839 |
+
key_features=prd_doc.key_features or {},
|
| 840 |
+
user_stories=prd_doc.user_stories or [],
|
| 841 |
+
assumptions=_normalize_assumptions(prd_doc.assumptions),
|
| 842 |
+
full_text=prd_doc.generated_prd or "",
|
| 843 |
+
judge_approved=prd_doc.judge_approved or False,
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
@router.get("/download/{session_id}")
|
| 848 |
+
def download_prd(
|
| 849 |
+
session_id: str,
|
| 850 |
+
format: str = Query(default="markdown", description="Format: markdown or pdf"),
|
| 851 |
+
db: Session = Depends(get_db),
|
| 852 |
+
):
|
| 853 |
+
"""Download the PRD document in Markdown or PDF format."""
|
| 854 |
+
prd_doc = db.query(PRDDocument).filter_by(session_id=session_id).first()
|
| 855 |
+
|
| 856 |
+
if not prd_doc or not prd_doc.generated_prd:
|
| 857 |
+
return JSONResponse(
|
| 858 |
+
status_code=404,
|
| 859 |
+
content={"error": "PRD not found or not generated"},
|
| 860 |
+
)
|
| 861 |
+
|
| 862 |
+
if format == "pdf":
|
| 863 |
+
from app.routers.web import generate_pdf_document
|
| 864 |
+
|
| 865 |
+
pdf_bytes = generate_pdf_document(
|
| 866 |
+
project_description=prd_doc.initial_description,
|
| 867 |
+
markdown_outputs={"product_owner": prd_doc.generated_prd},
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
return StreamingResponse(
|
| 871 |
+
BytesIO(pdf_bytes.getvalue()),
|
| 872 |
+
media_type="application/pdf",
|
| 873 |
+
headers={
|
| 874 |
+
"Content-Disposition": f"attachment; filename=prd_{session_id[:8]}.pdf"
|
| 875 |
+
},
|
| 876 |
+
)
|
| 877 |
+
|
| 878 |
+
return JSONResponse(
|
| 879 |
+
content={
|
| 880 |
+
"session_id": session_id,
|
| 881 |
+
"markdown": prd_doc.generated_prd,
|
| 882 |
+
},
|
| 883 |
+
headers={
|
| 884 |
+
"Content-Disposition": f"attachment; filename=prd_{session_id[:8]}.md"
|
| 885 |
+
},
|
| 886 |
+
)
|
| 887 |
+
|
| 888 |
+
|
| 889 |
+
@router.post("/pipeline/{session_id}", response_model=dict)
|
| 890 |
+
async def send_to_pipeline(
|
| 891 |
+
session_id: str, db: Session = Depends(get_db)
|
| 892 |
+
) -> dict[str, Any]:
|
| 893 |
+
"""Send the PRD to the multi-agent pipeline for full SRS generation."""
|
| 894 |
+
from app.routers.web import get_orchestrator
|
| 895 |
+
|
| 896 |
+
prd_doc = db.query(PRDDocument).filter_by(session_id=session_id).first()
|
| 897 |
+
|
| 898 |
+
if not prd_doc or not prd_doc.generated_prd:
|
| 899 |
+
return {"error": "PRD not found or not generated"}
|
| 900 |
+
|
| 901 |
+
if not prd_doc.judge_approved:
|
| 902 |
+
return {"error": "PRD must be approved by judge before sending to SRS"}
|
| 903 |
+
|
| 904 |
+
prd_context = {
|
| 905 |
+
"product_vision": prd_doc.product_vision or "",
|
| 906 |
+
"features": prd_doc.key_features or {},
|
| 907 |
+
"user_stories": prd_doc.user_stories or [],
|
| 908 |
+
"assumptions": prd_doc.assumptions or [],
|
| 909 |
+
"full_text": prd_doc.generated_prd,
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
project_req = ProjectRequest(
|
| 913 |
+
description=prd_doc.generated_prd,
|
| 914 |
+
)
|
| 915 |
+
|
| 916 |
+
orchestrator = get_orchestrator()
|
| 917 |
+
results = await orchestrator.run_pipeline(project_req, prd_context=prd_context)
|
| 918 |
+
|
| 919 |
+
return dict(results)
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
class JudgeDetailsResponse(BaseModel):
|
| 923 |
+
"""Response model for judge details endpoint."""
|
| 924 |
+
|
| 925 |
+
session_id: str
|
| 926 |
+
judge_score: int | None
|
| 927 |
+
judge_approved: bool | None
|
| 928 |
+
judge_feedback: str | None
|
| 929 |
+
issues: list[dict[str, str]] | None
|
| 930 |
+
|
| 931 |
+
|
| 932 |
+
@router.get("/judge/{session_id}", response_model=JudgeDetailsResponse)
|
| 933 |
+
def get_judge_details(
|
| 934 |
+
session_id: str,
|
| 935 |
+
db: Session = Depends(get_db),
|
| 936 |
+
) -> JudgeDetailsResponse:
|
| 937 |
+
"""
|
| 938 |
+
Get judge evaluation details for a PRD session.
|
| 939 |
+
|
| 940 |
+
This endpoint returns internal judge feedback that is not shown to the user
|
| 941 |
+
by default. Access this to review quality assessment details.
|
| 942 |
+
"""
|
| 943 |
+
prd_doc = db.query(PRDDocument).filter_by(session_id=session_id).first()
|
| 944 |
+
|
| 945 |
+
if not prd_doc:
|
| 946 |
+
return JudgeDetailsResponse(
|
| 947 |
+
session_id=session_id,
|
| 948 |
+
judge_score=None,
|
| 949 |
+
judge_approved=None,
|
| 950 |
+
judge_feedback=None,
|
| 951 |
+
issues=None,
|
| 952 |
+
)
|
| 953 |
+
|
| 954 |
+
# Parse issues from judge feedback if available
|
| 955 |
+
issues = None
|
| 956 |
+
if prd_doc.judge_feedback:
|
| 957 |
+
# Simple parsing - in production this might be JSON
|
| 958 |
+
issues = [{"type": "general", "suggestion": prd_doc.judge_feedback}]
|
| 959 |
+
|
| 960 |
+
return JudgeDetailsResponse(
|
| 961 |
+
session_id=session_id,
|
| 962 |
+
judge_score=prd_doc.judge_score,
|
| 963 |
+
judge_approved=prd_doc.judge_approved,
|
| 964 |
+
judge_feedback=prd_doc.judge_feedback,
|
| 965 |
+
issues=issues,
|
| 966 |
+
)
|
app/routers/web.py
CHANGED
|
@@ -253,8 +253,8 @@ def generate_pdf_document(
|
|
| 253 |
# --- Improved Markdown Block Handling ---
|
| 254 |
lines = content.split("\n")
|
| 255 |
in_code_block = False
|
| 256 |
-
code_block_lines = []
|
| 257 |
-
paragraph_lines = []
|
| 258 |
|
| 259 |
def flush_paragraph(lines_buffer):
|
| 260 |
if lines_buffer:
|
|
|
|
| 253 |
# --- Improved Markdown Block Handling ---
|
| 254 |
lines = content.split("\n")
|
| 255 |
in_code_block = False
|
| 256 |
+
code_block_lines: list[str] = []
|
| 257 |
+
paragraph_lines: list[str] = []
|
| 258 |
|
| 259 |
def flush_paragraph(lines_buffer):
|
| 260 |
if lines_buffer:
|
pyproject.toml
CHANGED
|
@@ -40,10 +40,6 @@ dependencies = [
|
|
| 40 |
# MongoDB Vector Store
|
| 41 |
"pymongo>=4.6.0",
|
| 42 |
"langchain-mongodb>=0.1.0",
|
| 43 |
-
"hydra-core>=1.3.2",
|
| 44 |
-
"lightning>=2.6.0",
|
| 45 |
-
"fiddle>=0.3.0",
|
| 46 |
-
"cloudpickle>=3.1.2",
|
| 47 |
"upstash-redis>=1.5.0",
|
| 48 |
]
|
| 49 |
|
|
|
|
| 40 |
# MongoDB Vector Store
|
| 41 |
"pymongo>=4.6.0",
|
| 42 |
"langchain-mongodb>=0.1.0",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
"upstash-redis>=1.5.0",
|
| 44 |
]
|
| 45 |
|
requirements.txt
CHANGED
|
@@ -1,23 +1,15 @@
|
|
| 1 |
# This file was autogenerated by uv via the following command:
|
| 2 |
# uv pip compile pyproject.toml --output-file requirements.txt
|
| 3 |
-
absl-py==2.3.1
|
| 4 |
-
# via fiddle
|
| 5 |
aiohappyeyeballs==2.6.1
|
| 6 |
# via aiohttp
|
| 7 |
aiohttp==3.13.2
|
| 8 |
-
# via
|
| 9 |
-
# fsspec
|
| 10 |
-
# langchain-nvidia-ai-endpoints
|
| 11 |
aiosignal==1.4.0
|
| 12 |
# via aiohttp
|
| 13 |
annotated-doc==0.0.4
|
| 14 |
# via fastapi
|
| 15 |
annotated-types==0.7.0
|
| 16 |
# via pydantic
|
| 17 |
-
antlr4-python3-runtime==4.9.3
|
| 18 |
-
# via
|
| 19 |
-
# hydra-core
|
| 20 |
-
# omegaconf
|
| 21 |
anyio==4.12.0
|
| 22 |
# via
|
| 23 |
# httpx
|
|
@@ -39,12 +31,8 @@ charset-normalizer==3.4.4
|
|
| 39 |
# via requests
|
| 40 |
click==8.3.1
|
| 41 |
# via uvicorn
|
| 42 |
-
cloudpickle==3.1.2
|
| 43 |
-
# via specs-before-code-api (pyproject.toml)
|
| 44 |
colorama==0.4.6
|
| 45 |
-
# via
|
| 46 |
-
# click
|
| 47 |
-
# tqdm
|
| 48 |
cryptography==46.0.3
|
| 49 |
# via python-jose
|
| 50 |
defusedxml==0.7.1
|
|
@@ -55,10 +43,6 @@ ecdsa==0.19.1
|
|
| 55 |
# via python-jose
|
| 56 |
fastapi==0.123.5
|
| 57 |
# via specs-before-code-api (pyproject.toml)
|
| 58 |
-
fiddle==0.3.0
|
| 59 |
-
# via specs-before-code-api (pyproject.toml)
|
| 60 |
-
filelock==3.20.2
|
| 61 |
-
# via torch
|
| 62 |
filetype==1.2.0
|
| 63 |
# via langchain-nvidia-ai-endpoints
|
| 64 |
fonttools==4.61.0
|
|
@@ -69,11 +53,6 @@ frozenlist==1.8.0
|
|
| 69 |
# via
|
| 70 |
# aiohttp
|
| 71 |
# aiosignal
|
| 72 |
-
fsspec==2025.12.0
|
| 73 |
-
# via
|
| 74 |
-
# lightning
|
| 75 |
-
# pytorch-lightning
|
| 76 |
-
# torch
|
| 77 |
google-api-core==2.28.1
|
| 78 |
# via google-api-python-client
|
| 79 |
google-api-python-client==2.187.0
|
|
@@ -90,8 +69,6 @@ google-auth-oauthlib==1.2.3
|
|
| 90 |
# via specs-before-code-api (pyproject.toml)
|
| 91 |
googleapis-common-protos==1.72.0
|
| 92 |
# via google-api-core
|
| 93 |
-
graphviz==0.21
|
| 94 |
-
# via fiddle
|
| 95 |
greenlet==3.3.0
|
| 96 |
# via sqlalchemy
|
| 97 |
h11==0.16.0
|
|
@@ -109,8 +86,6 @@ httpx==0.28.1
|
|
| 109 |
# langgraph-sdk
|
| 110 |
# langsmith
|
| 111 |
# upstash-redis
|
| 112 |
-
hydra-core==1.3.2
|
| 113 |
-
# via specs-before-code-api (pyproject.toml)
|
| 114 |
idna==3.11
|
| 115 |
# via
|
| 116 |
# anyio
|
|
@@ -118,9 +93,7 @@ idna==3.11
|
|
| 118 |
# requests
|
| 119 |
# yarl
|
| 120 |
jinja2==3.1.6
|
| 121 |
-
# via
|
| 122 |
-
# specs-before-code-api (pyproject.toml)
|
| 123 |
-
# torch
|
| 124 |
jsonpatch==1.33
|
| 125 |
# via langchain-core
|
| 126 |
jsonpointer==3.0.0
|
|
@@ -168,34 +141,18 @@ langsmith==0.4.53
|
|
| 168 |
# langchain-core
|
| 169 |
lark==1.3.1
|
| 170 |
# via langchain-mongodb
|
| 171 |
-
libcst==1.8.6
|
| 172 |
-
# via fiddle
|
| 173 |
-
lightning==2.6.0
|
| 174 |
-
# via specs-before-code-api (pyproject.toml)
|
| 175 |
-
lightning-utilities==0.15.2
|
| 176 |
-
# via
|
| 177 |
-
# lightning
|
| 178 |
-
# pytorch-lightning
|
| 179 |
-
# torchmetrics
|
| 180 |
markupsafe==3.0.3
|
| 181 |
# via jinja2
|
| 182 |
-
mpmath==1.3.0
|
| 183 |
-
# via sympy
|
| 184 |
multidict==6.7.0
|
| 185 |
# via
|
| 186 |
# aiohttp
|
| 187 |
# yarl
|
| 188 |
-
networkx==3.6.1
|
| 189 |
-
# via torch
|
| 190 |
numpy==2.3.5
|
| 191 |
# via
|
| 192 |
# specs-before-code-api (pyproject.toml)
|
| 193 |
# langchain-mongodb
|
| 194 |
-
# torchmetrics
|
| 195 |
oauthlib==3.3.1
|
| 196 |
# via requests-oauthlib
|
| 197 |
-
omegaconf==2.3.0
|
| 198 |
-
# via hydra-core
|
| 199 |
orjson==3.11.4
|
| 200 |
# via
|
| 201 |
# langgraph-sdk
|
|
@@ -204,13 +161,8 @@ ormsgpack==1.12.0
|
|
| 204 |
# via langgraph-checkpoint
|
| 205 |
packaging==24.2
|
| 206 |
# via
|
| 207 |
-
# hydra-core
|
| 208 |
# langchain-core
|
| 209 |
# langsmith
|
| 210 |
-
# lightning
|
| 211 |
-
# lightning-utilities
|
| 212 |
-
# pytorch-lightning
|
| 213 |
-
# torchmetrics
|
| 214 |
passlib==1.7.4
|
| 215 |
# via specs-before-code-api (pyproject.toml)
|
| 216 |
pillow==12.0.0
|
|
@@ -263,16 +215,10 @@ python-jose==3.5.0
|
|
| 263 |
# via specs-before-code-api (pyproject.toml)
|
| 264 |
python-multipart==0.0.20
|
| 265 |
# via specs-before-code-api (pyproject.toml)
|
| 266 |
-
pytorch-lightning==2.6.0
|
| 267 |
-
# via lightning
|
| 268 |
pyyaml==6.0.3
|
| 269 |
# via
|
| 270 |
# langchain-classic
|
| 271 |
# langchain-core
|
| 272 |
-
# lightning
|
| 273 |
-
# omegaconf
|
| 274 |
-
# pytorch-lightning
|
| 275 |
-
# via libcst
|
| 276 |
requests==2.32.5
|
| 277 |
# via
|
| 278 |
# google-api-core
|
|
@@ -288,10 +234,6 @@ rsa==4.9.1
|
|
| 288 |
# via
|
| 289 |
# google-auth
|
| 290 |
# python-jose
|
| 291 |
-
setuptools==80.9.0
|
| 292 |
-
# via
|
| 293 |
-
# lightning-utilities
|
| 294 |
-
# torch
|
| 295 |
six==1.17.0
|
| 296 |
# via ecdsa
|
| 297 |
sqlalchemy==2.0.45
|
|
@@ -300,35 +242,15 @@ sqlalchemy==2.0.45
|
|
| 300 |
# langchain-classic
|
| 301 |
starlette==0.50.0
|
| 302 |
# via fastapi
|
| 303 |
-
sympy==1.14.0
|
| 304 |
-
# via torch
|
| 305 |
tenacity==9.1.2
|
| 306 |
# via langchain-core
|
| 307 |
-
torch==2.9.1
|
| 308 |
-
# via
|
| 309 |
-
# lightning
|
| 310 |
-
# pytorch-lightning
|
| 311 |
-
# torchmetrics
|
| 312 |
-
torchmetrics==1.8.2
|
| 313 |
-
# via
|
| 314 |
-
# lightning
|
| 315 |
-
# pytorch-lightning
|
| 316 |
-
tqdm==4.67.1
|
| 317 |
-
# via
|
| 318 |
-
# lightning
|
| 319 |
-
# pytorch-lightning
|
| 320 |
typing-extensions==4.15.0
|
| 321 |
# via
|
| 322 |
# fastapi
|
| 323 |
-
# fiddle
|
| 324 |
# langchain-core
|
| 325 |
-
# lightning
|
| 326 |
-
# lightning-utilities
|
| 327 |
# pydantic
|
| 328 |
# pydantic-core
|
| 329 |
-
# pytorch-lightning
|
| 330 |
# sqlalchemy
|
| 331 |
-
# torch
|
| 332 |
# typing-inspection
|
| 333 |
typing-inspection==0.4.2
|
| 334 |
# via pydantic
|
|
|
|
| 1 |
# This file was autogenerated by uv via the following command:
|
| 2 |
# uv pip compile pyproject.toml --output-file requirements.txt
|
|
|
|
|
|
|
| 3 |
aiohappyeyeballs==2.6.1
|
| 4 |
# via aiohttp
|
| 5 |
aiohttp==3.13.2
|
| 6 |
+
# via langchain-nvidia-ai-endpoints
|
|
|
|
|
|
|
| 7 |
aiosignal==1.4.0
|
| 8 |
# via aiohttp
|
| 9 |
annotated-doc==0.0.4
|
| 10 |
# via fastapi
|
| 11 |
annotated-types==0.7.0
|
| 12 |
# via pydantic
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
anyio==4.12.0
|
| 14 |
# via
|
| 15 |
# httpx
|
|
|
|
| 31 |
# via requests
|
| 32 |
click==8.3.1
|
| 33 |
# via uvicorn
|
|
|
|
|
|
|
| 34 |
colorama==0.4.6
|
| 35 |
+
# via click
|
|
|
|
|
|
|
| 36 |
cryptography==46.0.3
|
| 37 |
# via python-jose
|
| 38 |
defusedxml==0.7.1
|
|
|
|
| 43 |
# via python-jose
|
| 44 |
fastapi==0.123.5
|
| 45 |
# via specs-before-code-api (pyproject.toml)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
filetype==1.2.0
|
| 47 |
# via langchain-nvidia-ai-endpoints
|
| 48 |
fonttools==4.61.0
|
|
|
|
| 53 |
# via
|
| 54 |
# aiohttp
|
| 55 |
# aiosignal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
google-api-core==2.28.1
|
| 57 |
# via google-api-python-client
|
| 58 |
google-api-python-client==2.187.0
|
|
|
|
| 69 |
# via specs-before-code-api (pyproject.toml)
|
| 70 |
googleapis-common-protos==1.72.0
|
| 71 |
# via google-api-core
|
|
|
|
|
|
|
| 72 |
greenlet==3.3.0
|
| 73 |
# via sqlalchemy
|
| 74 |
h11==0.16.0
|
|
|
|
| 86 |
# langgraph-sdk
|
| 87 |
# langsmith
|
| 88 |
# upstash-redis
|
|
|
|
|
|
|
| 89 |
idna==3.11
|
| 90 |
# via
|
| 91 |
# anyio
|
|
|
|
| 93 |
# requests
|
| 94 |
# yarl
|
| 95 |
jinja2==3.1.6
|
| 96 |
+
# via specs-before-code-api (pyproject.toml)
|
|
|
|
|
|
|
| 97 |
jsonpatch==1.33
|
| 98 |
# via langchain-core
|
| 99 |
jsonpointer==3.0.0
|
|
|
|
| 141 |
# langchain-core
|
| 142 |
lark==1.3.1
|
| 143 |
# via langchain-mongodb
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
markupsafe==3.0.3
|
| 145 |
# via jinja2
|
|
|
|
|
|
|
| 146 |
multidict==6.7.0
|
| 147 |
# via
|
| 148 |
# aiohttp
|
| 149 |
# yarl
|
|
|
|
|
|
|
| 150 |
numpy==2.3.5
|
| 151 |
# via
|
| 152 |
# specs-before-code-api (pyproject.toml)
|
| 153 |
# langchain-mongodb
|
|
|
|
| 154 |
oauthlib==3.3.1
|
| 155 |
# via requests-oauthlib
|
|
|
|
|
|
|
| 156 |
orjson==3.11.4
|
| 157 |
# via
|
| 158 |
# langgraph-sdk
|
|
|
|
| 161 |
# via langgraph-checkpoint
|
| 162 |
packaging==24.2
|
| 163 |
# via
|
|
|
|
| 164 |
# langchain-core
|
| 165 |
# langsmith
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
passlib==1.7.4
|
| 167 |
# via specs-before-code-api (pyproject.toml)
|
| 168 |
pillow==12.0.0
|
|
|
|
| 215 |
# via specs-before-code-api (pyproject.toml)
|
| 216 |
python-multipart==0.0.20
|
| 217 |
# via specs-before-code-api (pyproject.toml)
|
|
|
|
|
|
|
| 218 |
pyyaml==6.0.3
|
| 219 |
# via
|
| 220 |
# langchain-classic
|
| 221 |
# langchain-core
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
requests==2.32.5
|
| 223 |
# via
|
| 224 |
# google-api-core
|
|
|
|
| 234 |
# via
|
| 235 |
# google-auth
|
| 236 |
# python-jose
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
six==1.17.0
|
| 238 |
# via ecdsa
|
| 239 |
sqlalchemy==2.0.45
|
|
|
|
| 242 |
# langchain-classic
|
| 243 |
starlette==0.50.0
|
| 244 |
# via fastapi
|
|
|
|
|
|
|
| 245 |
tenacity==9.1.2
|
| 246 |
# via langchain-core
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
typing-extensions==4.15.0
|
| 248 |
# via
|
| 249 |
# fastapi
|
|
|
|
| 250 |
# langchain-core
|
|
|
|
|
|
|
| 251 |
# pydantic
|
| 252 |
# pydantic-core
|
|
|
|
| 253 |
# sqlalchemy
|
|
|
|
| 254 |
# typing-inspection
|
| 255 |
typing-inspection==0.4.2
|
| 256 |
# via pydantic
|
tests/test_prd_endpoints.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.testclient import TestClient
|
| 2 |
+
|
| 3 |
+
from app.core.database import Base, engine
|
| 4 |
+
from app.main import app
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _reset_db():
|
| 8 |
+
Base.metadata.drop_all(bind=engine)
|
| 9 |
+
Base.metadata.create_all(bind=engine)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _start_session(client: TestClient, user_id: int = 1) -> str:
|
| 13 |
+
resp = client.post(
|
| 14 |
+
"/prd/start",
|
| 15 |
+
json={"description": "Test PRD", "user_id": user_id},
|
| 16 |
+
)
|
| 17 |
+
assert resp.status_code == 200
|
| 18 |
+
return resp.json()["session_id"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Happy path: PRD chat endpoint
|
| 22 |
+
def test_prd_chat_happy():
|
| 23 |
+
_reset_db()
|
| 24 |
+
client = TestClient(app)
|
| 25 |
+
session_id = _start_session(client)
|
| 26 |
+
|
| 27 |
+
resp = client.post(
|
| 28 |
+
"/prd/chat",
|
| 29 |
+
json={
|
| 30 |
+
"session_id": session_id,
|
| 31 |
+
"message": "Our main goal is to improve user experience.",
|
| 32 |
+
},
|
| 33 |
+
)
|
| 34 |
+
assert resp.status_code == 200
|
| 35 |
+
data = resp.json()
|
| 36 |
+
assert "agent_response" in data
|
| 37 |
+
assert "missing_requirements" in data
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# Sad path: PRD chat with empty message
|
| 41 |
+
def test_prd_chat_empty():
|
| 42 |
+
_reset_db()
|
| 43 |
+
client = TestClient(app)
|
| 44 |
+
session_id = _start_session(client)
|
| 45 |
+
|
| 46 |
+
resp = client.post("/prd/chat", json={"session_id": session_id, "message": ""})
|
| 47 |
+
assert resp.status_code == 200
|
| 48 |
+
data = resp.json()
|
| 49 |
+
assert "agent_response" in data
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# Happy path: PRD doc endpoint
|
| 53 |
+
def test_prd_doc_happy():
|
| 54 |
+
_reset_db()
|
| 55 |
+
client = TestClient(app)
|
| 56 |
+
session_id = _start_session(client)
|
| 57 |
+
|
| 58 |
+
resp = client.get(f"/prd/doc/{session_id}")
|
| 59 |
+
assert resp.status_code == 200
|
| 60 |
+
data = resp.json()
|
| 61 |
+
assert data["session_id"] == session_id
|
| 62 |
+
assert "generated_prd" in data
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# Sad path: PRD doc with missing session_id
|
| 66 |
+
def test_prd_doc_missing():
|
| 67 |
+
_reset_db()
|
| 68 |
+
client = TestClient(app)
|
| 69 |
+
|
| 70 |
+
resp = client.get("/prd/doc")
|
| 71 |
+
assert resp.status_code == 404
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# Happy path: PRD status endpoint
|
| 75 |
+
def test_prd_status_happy():
|
| 76 |
+
_reset_db()
|
| 77 |
+
client = TestClient(app)
|
| 78 |
+
session_id = _start_session(client)
|
| 79 |
+
|
| 80 |
+
resp = client.get(f"/prd/status/{session_id}")
|
| 81 |
+
assert resp.status_code == 200
|
| 82 |
+
data = resp.json()
|
| 83 |
+
assert data["session_id"] == session_id
|
| 84 |
+
assert "requirements_status" in data
|
| 85 |
+
assert "missing_sections" in data
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# Sad path: PRD status with missing session_id
|
| 89 |
+
def test_prd_status_missing():
|
| 90 |
+
_reset_db()
|
| 91 |
+
client = TestClient(app)
|
| 92 |
+
|
| 93 |
+
resp = client.get("/prd/status")
|
| 94 |
+
assert resp.status_code == 404
|
tests/test_rag.py
CHANGED
|
@@ -21,7 +21,7 @@ class TestRAGServiceInit:
|
|
| 21 |
"""RAGService should initialize embeddings."""
|
| 22 |
mock_embeddings.return_value = MagicMock()
|
| 23 |
|
| 24 |
-
with patch.object(RAGService, "
|
| 25 |
service = RAGService()
|
| 26 |
|
| 27 |
assert mock_embeddings.called
|
|
@@ -34,7 +34,7 @@ class TestRAGServiceInit:
|
|
| 34 |
mock_embeddings.return_value = MagicMock()
|
| 35 |
mock_getenv.return_value = None # No Pinecone config
|
| 36 |
|
| 37 |
-
with patch.object(RAGService, "
|
| 38 |
service = RAGService()
|
| 39 |
|
| 40 |
# Should attempt fallback
|
|
@@ -63,7 +63,7 @@ class TestRAGServiceRetrieve:
|
|
| 63 |
"""Retrieve should handle missing vector store gracefully."""
|
| 64 |
mock_embeddings.return_value = MagicMock()
|
| 65 |
|
| 66 |
-
with patch.object(RAGService, "
|
| 67 |
service = RAGService()
|
| 68 |
service.vector_store = None
|
| 69 |
|
|
@@ -75,11 +75,11 @@ class TestRAGServiceRetrieve:
|
|
| 75 |
"""format_docs should handle empty list."""
|
| 76 |
mock_embeddings.return_value = MagicMock()
|
| 77 |
|
| 78 |
-
with patch.object(RAGService, "
|
| 79 |
service = RAGService()
|
| 80 |
|
| 81 |
result = service.format_docs([])
|
| 82 |
-
assert "No relevant
|
| 83 |
|
| 84 |
|
| 85 |
class TestRAGServiceFormatDocs:
|
|
@@ -92,7 +92,7 @@ class TestRAGServiceFormatDocs:
|
|
| 92 |
|
| 93 |
mock_embeddings.return_value = MagicMock()
|
| 94 |
|
| 95 |
-
with patch.object(RAGService, "
|
| 96 |
service = RAGService()
|
| 97 |
|
| 98 |
doc = Document(
|
|
@@ -111,7 +111,7 @@ class TestRAGServiceFormatDocs:
|
|
| 111 |
|
| 112 |
mock_embeddings.return_value = MagicMock()
|
| 113 |
|
| 114 |
-
with patch.object(RAGService, "
|
| 115 |
service = RAGService()
|
| 116 |
|
| 117 |
docs = [
|
|
@@ -134,12 +134,16 @@ class TestRAGServiceRetriever:
|
|
| 134 |
"""get_retriever should raise if no vector store."""
|
| 135 |
mock_embeddings.return_value = MagicMock()
|
| 136 |
|
| 137 |
-
with patch
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
|
| 144 |
@patch("app.core.rag.get_embeddings_model")
|
| 145 |
def test_get_retriever_with_store(self, mock_embeddings):
|
|
@@ -149,9 +153,11 @@ class TestRAGServiceRetriever:
|
|
| 149 |
mock_retriever = MagicMock()
|
| 150 |
mock_store.as_retriever.return_value = mock_retriever
|
| 151 |
|
| 152 |
-
with patch.object(RAGService, "
|
| 153 |
service = RAGService()
|
| 154 |
-
|
|
|
|
|
|
|
| 155 |
|
| 156 |
retriever = service.get_retriever(k=5)
|
| 157 |
|
|
|
|
| 21 |
"""RAGService should initialize embeddings."""
|
| 22 |
mock_embeddings.return_value = MagicMock()
|
| 23 |
|
| 24 |
+
with patch.object(RAGService, "_init_fallback_store"):
|
| 25 |
service = RAGService()
|
| 26 |
|
| 27 |
assert mock_embeddings.called
|
|
|
|
| 34 |
mock_embeddings.return_value = MagicMock()
|
| 35 |
mock_getenv.return_value = None # No Pinecone config
|
| 36 |
|
| 37 |
+
with patch.object(RAGService, "_init_fallback_store") as mock_fallback:
|
| 38 |
service = RAGService()
|
| 39 |
|
| 40 |
# Should attempt fallback
|
|
|
|
| 63 |
"""Retrieve should handle missing vector store gracefully."""
|
| 64 |
mock_embeddings.return_value = MagicMock()
|
| 65 |
|
| 66 |
+
with patch.object(RAGService, "_init_fallback_store"):
|
| 67 |
service = RAGService()
|
| 68 |
service.vector_store = None
|
| 69 |
|
|
|
|
| 75 |
"""format_docs should handle empty list."""
|
| 76 |
mock_embeddings.return_value = MagicMock()
|
| 77 |
|
| 78 |
+
with patch.object(RAGService, "_init_fallback_store"):
|
| 79 |
service = RAGService()
|
| 80 |
|
| 81 |
result = service.format_docs([])
|
| 82 |
+
assert "No relevant examples found in knowledge base." in result
|
| 83 |
|
| 84 |
|
| 85 |
class TestRAGServiceFormatDocs:
|
|
|
|
| 92 |
|
| 93 |
mock_embeddings.return_value = MagicMock()
|
| 94 |
|
| 95 |
+
with patch.object(RAGService, "_init_fallback_store"):
|
| 96 |
service = RAGService()
|
| 97 |
|
| 98 |
doc = Document(
|
|
|
|
| 111 |
|
| 112 |
mock_embeddings.return_value = MagicMock()
|
| 113 |
|
| 114 |
+
with patch.object(RAGService, "_init_fallback_store"):
|
| 115 |
service = RAGService()
|
| 116 |
|
| 117 |
docs = [
|
|
|
|
| 134 |
"""get_retriever should raise if no vector store."""
|
| 135 |
mock_embeddings.return_value = MagicMock()
|
| 136 |
|
| 137 |
+
with patch("app.core.rag.os.getenv") as mock_getenv:
|
| 138 |
+
mock_getenv.return_value = None # Simulate no MONGODB_URI
|
| 139 |
+
with patch.object(RAGService, "_init_fallback_store"):
|
| 140 |
+
service = RAGService()
|
| 141 |
+
# Simulate both stores missing
|
| 142 |
+
service._mongodb_service = None
|
| 143 |
+
service._fallback_store = None
|
| 144 |
|
| 145 |
+
with pytest.raises(RuntimeError, match="not initialized"):
|
| 146 |
+
service.get_retriever()
|
| 147 |
|
| 148 |
@patch("app.core.rag.get_embeddings_model")
|
| 149 |
def test_get_retriever_with_store(self, mock_embeddings):
|
|
|
|
| 153 |
mock_retriever = MagicMock()
|
| 154 |
mock_store.as_retriever.return_value = mock_retriever
|
| 155 |
|
| 156 |
+
with patch.object(RAGService, "_init_fallback_store"):
|
| 157 |
service = RAGService()
|
| 158 |
+
# Simulate fallback store being used
|
| 159 |
+
service._mongodb_service = None
|
| 160 |
+
service._fallback_store = mock_store
|
| 161 |
|
| 162 |
retriever = service.get_retriever(k=5)
|
| 163 |
|
uv.lock
CHANGED
|
@@ -7,15 +7,6 @@ resolution-markers = [
|
|
| 7 |
"python_full_version < '3.13'",
|
| 8 |
]
|
| 9 |
|
| 10 |
-
[[package]]
|
| 11 |
-
name = "absl-py"
|
| 12 |
-
version = "2.3.1"
|
| 13 |
-
source = { registry = "https://pypi.org/simple" }
|
| 14 |
-
sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" }
|
| 15 |
-
wheels = [
|
| 16 |
-
{ url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" },
|
| 17 |
-
]
|
| 18 |
-
|
| 19 |
[[package]]
|
| 20 |
name = "aiohappyeyeballs"
|
| 21 |
version = "2.6.1"
|
|
@@ -141,12 +132,6 @@ wheels = [
|
|
| 141 |
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
| 142 |
]
|
| 143 |
|
| 144 |
-
[[package]]
|
| 145 |
-
name = "antlr4-python3-runtime"
|
| 146 |
-
version = "4.9.3"
|
| 147 |
-
source = { registry = "https://pypi.org/simple" }
|
| 148 |
-
sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" }
|
| 149 |
-
|
| 150 |
[[package]]
|
| 151 |
name = "anyio"
|
| 152 |
version = "4.12.0"
|
|
@@ -411,15 +396,6 @@ wheels = [
|
|
| 411 |
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
| 412 |
]
|
| 413 |
|
| 414 |
-
[[package]]
|
| 415 |
-
name = "cloudpickle"
|
| 416 |
-
version = "3.1.2"
|
| 417 |
-
source = { registry = "https://pypi.org/simple" }
|
| 418 |
-
sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
|
| 419 |
-
wheels = [
|
| 420 |
-
{ url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
|
| 421 |
-
]
|
| 422 |
-
|
| 423 |
[[package]]
|
| 424 |
name = "colorama"
|
| 425 |
version = "0.4.6"
|
|
@@ -604,30 +580,6 @@ wheels = [
|
|
| 604 |
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
| 605 |
]
|
| 606 |
|
| 607 |
-
[[package]]
|
| 608 |
-
name = "fiddle"
|
| 609 |
-
version = "0.3.0"
|
| 610 |
-
source = { registry = "https://pypi.org/simple" }
|
| 611 |
-
dependencies = [
|
| 612 |
-
{ name = "absl-py" },
|
| 613 |
-
{ name = "graphviz" },
|
| 614 |
-
{ name = "libcst" },
|
| 615 |
-
{ name = "typing-extensions" },
|
| 616 |
-
]
|
| 617 |
-
sdist = { url = "https://files.pythonhosted.org/packages/73/36/7a4fac76351619b36bbc7937abf59f7b601326dc4efc253b3c16819f782a/fiddle-0.3.0.tar.gz", hash = "sha256:5d083d3299a479868345513385a6c5546141bd92086c15d3dcbf8008a90075d3", size = 277884, upload-time = "2024-04-09T17:23:58.974Z" }
|
| 618 |
-
wheels = [
|
| 619 |
-
{ url = "https://files.pythonhosted.org/packages/3b/98/a38e949a91ff9e15874487fd8329ff53c25f3413c0cfc809eb6ff7eb7fa1/fiddle-0.3.0-py3-none-any.whl", hash = "sha256:f4824541c103a94a2f33f6c93eeddf6007c3a7300440087a95907f3e74362e61", size = 419830, upload-time = "2024-04-09T17:23:56.7Z" },
|
| 620 |
-
]
|
| 621 |
-
|
| 622 |
-
[[package]]
|
| 623 |
-
name = "filelock"
|
| 624 |
-
version = "3.20.1"
|
| 625 |
-
source = { registry = "https://pypi.org/simple" }
|
| 626 |
-
sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" }
|
| 627 |
-
wheels = [
|
| 628 |
-
{ url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" },
|
| 629 |
-
]
|
| 630 |
-
|
| 631 |
[[package]]
|
| 632 |
name = "filetype"
|
| 633 |
version = "1.2.0"
|
|
@@ -781,20 +733,6 @@ wheels = [
|
|
| 781 |
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
| 782 |
]
|
| 783 |
|
| 784 |
-
[[package]]
|
| 785 |
-
name = "fsspec"
|
| 786 |
-
version = "2025.12.0"
|
| 787 |
-
source = { registry = "https://pypi.org/simple" }
|
| 788 |
-
sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" }
|
| 789 |
-
wheels = [
|
| 790 |
-
{ url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" },
|
| 791 |
-
]
|
| 792 |
-
|
| 793 |
-
[package.optional-dependencies]
|
| 794 |
-
http = [
|
| 795 |
-
{ name = "aiohttp" },
|
| 796 |
-
]
|
| 797 |
-
|
| 798 |
[[package]]
|
| 799 |
name = "google-api-core"
|
| 800 |
version = "2.28.1"
|
|
@@ -879,15 +817,6 @@ wheels = [
|
|
| 879 |
{ url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
|
| 880 |
]
|
| 881 |
|
| 882 |
-
[[package]]
|
| 883 |
-
name = "graphviz"
|
| 884 |
-
version = "0.21"
|
| 885 |
-
source = { registry = "https://pypi.org/simple" }
|
| 886 |
-
sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" }
|
| 887 |
-
wheels = [
|
| 888 |
-
{ url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" },
|
| 889 |
-
]
|
| 890 |
-
|
| 891 |
[[package]]
|
| 892 |
name = "greenlet"
|
| 893 |
version = "3.3.0"
|
|
@@ -976,20 +905,6 @@ wheels = [
|
|
| 976 |
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
| 977 |
]
|
| 978 |
|
| 979 |
-
[[package]]
|
| 980 |
-
name = "hydra-core"
|
| 981 |
-
version = "1.3.2"
|
| 982 |
-
source = { registry = "https://pypi.org/simple" }
|
| 983 |
-
dependencies = [
|
| 984 |
-
{ name = "antlr4-python3-runtime" },
|
| 985 |
-
{ name = "omegaconf" },
|
| 986 |
-
{ name = "packaging" },
|
| 987 |
-
]
|
| 988 |
-
sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" }
|
| 989 |
-
wheels = [
|
| 990 |
-
{ url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" },
|
| 991 |
-
]
|
| 992 |
-
|
| 993 |
[[package]]
|
| 994 |
name = "idna"
|
| 995 |
version = "3.11"
|
|
@@ -1221,58 +1136,6 @@ wheels = [
|
|
| 1221 |
{ url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" },
|
| 1222 |
]
|
| 1223 |
|
| 1224 |
-
[[package]]
|
| 1225 |
-
name = "libcst"
|
| 1226 |
-
version = "1.8.6"
|
| 1227 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1228 |
-
dependencies = [
|
| 1229 |
-
{ name = "pyyaml", marker = "python_full_version != '3.13.*'" },
|
| 1230 |
-
{ name = "pyyaml-ft", marker = "python_full_version == '3.13.*'" },
|
| 1231 |
-
]
|
| 1232 |
-
sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" }
|
| 1233 |
-
wheels = [
|
| 1234 |
-
{ url = "https://files.pythonhosted.org/packages/0c/3c/93365c17da3d42b055a8edb0e1e99f1c60c776471db6c9b7f1ddf6a44b28/libcst-1.8.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9", size = 2206166, upload-time = "2025-11-03T22:32:16.012Z" },
|
| 1235 |
-
{ url = "https://files.pythonhosted.org/packages/1d/cb/7530940e6ac50c6dd6022349721074e19309eb6aa296e942ede2213c1a19/libcst-1.8.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09", size = 2083726, upload-time = "2025-11-03T22:32:17.312Z" },
|
| 1236 |
-
{ url = "https://files.pythonhosted.org/packages/1b/cf/7e5eaa8c8f2c54913160671575351d129170db757bb5e4b7faffed022271/libcst-1.8.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d", size = 2235755, upload-time = "2025-11-03T22:32:18.859Z" },
|
| 1237 |
-
{ url = "https://files.pythonhosted.org/packages/55/54/570ec2b0e9a3de0af9922e3bb1b69a5429beefbc753a7ea770a27ad308bd/libcst-1.8.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5", size = 2301473, upload-time = "2025-11-03T22:32:20.499Z" },
|
| 1238 |
-
{ url = "https://files.pythonhosted.org/packages/11/4c/163457d1717cd12181c421a4cca493454bcabd143fc7e53313bc6a4ad82a/libcst-1.8.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1", size = 2298899, upload-time = "2025-11-03T22:32:21.765Z" },
|
| 1239 |
-
{ url = "https://files.pythonhosted.org/packages/35/1d/317ddef3669883619ef3d3395ea583305f353ef4ad87d7a5ac1c39be38e3/libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86", size = 2408239, upload-time = "2025-11-03T22:32:23.275Z" },
|
| 1240 |
-
{ url = "https://files.pythonhosted.org/packages/9a/a1/f47d8cccf74e212dd6044b9d6dbc223636508da99acff1d54786653196bc/libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d", size = 2119660, upload-time = "2025-11-03T22:32:24.822Z" },
|
| 1241 |
-
{ url = "https://files.pythonhosted.org/packages/19/d0/dd313bf6a7942cdf951828f07ecc1a7695263f385065edc75ef3016a3cb5/libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7", size = 1999824, upload-time = "2025-11-03T22:32:26.131Z" },
|
| 1242 |
-
{ url = "https://files.pythonhosted.org/packages/90/01/723cd467ec267e712480c772aacc5aa73f82370c9665162fd12c41b0065b/libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb", size = 2206386, upload-time = "2025-11-03T22:32:27.422Z" },
|
| 1243 |
-
{ url = "https://files.pythonhosted.org/packages/17/50/b944944f910f24c094f9b083f76f61e3985af5a376f5342a21e01e2d1a81/libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196", size = 2083945, upload-time = "2025-11-03T22:32:28.847Z" },
|
| 1244 |
-
{ url = "https://files.pythonhosted.org/packages/36/a1/bd1b2b2b7f153d82301cdaddba787f4a9fc781816df6bdb295ca5f88b7cf/libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105", size = 2235818, upload-time = "2025-11-03T22:32:30.504Z" },
|
| 1245 |
-
{ url = "https://files.pythonhosted.org/packages/b9/ab/f5433988acc3b4d188c4bb154e57837df9488cc9ab551267cdeabd3bb5e7/libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d", size = 2301289, upload-time = "2025-11-03T22:32:31.812Z" },
|
| 1246 |
-
{ url = "https://files.pythonhosted.org/packages/5d/57/89f4ba7a6f1ac274eec9903a9e9174890d2198266eee8c00bc27eb45ecf7/libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786", size = 2299230, upload-time = "2025-11-03T22:32:33.242Z" },
|
| 1247 |
-
{ url = "https://files.pythonhosted.org/packages/f2/36/0aa693bc24cce163a942df49d36bf47a7ed614a0cd5598eee2623bc31913/libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30", size = 2408519, upload-time = "2025-11-03T22:32:34.678Z" },
|
| 1248 |
-
{ url = "https://files.pythonhosted.org/packages/db/18/6dd055b5f15afa640fb3304b2ee9df8b7f72e79513814dbd0a78638f4a0e/libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde", size = 2119853, upload-time = "2025-11-03T22:32:36.287Z" },
|
| 1249 |
-
{ url = "https://files.pythonhosted.org/packages/c9/ed/5ddb2a22f0b0abdd6dcffa40621ada1feaf252a15e5b2733a0a85dfd0429/libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf", size = 1999808, upload-time = "2025-11-03T22:32:38.1Z" },
|
| 1250 |
-
{ url = "https://files.pythonhosted.org/packages/25/d3/72b2de2c40b97e1ef4a1a1db4e5e52163fc7e7740ffef3846d30bc0096b5/libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e", size = 2190553, upload-time = "2025-11-03T22:32:39.819Z" },
|
| 1251 |
-
{ url = "https://files.pythonhosted.org/packages/0d/20/983b7b210ccc3ad94a82db54230e92599c4a11b9cfc7ce3bc97c1d2df75c/libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58", size = 2074717, upload-time = "2025-11-03T22:32:41.373Z" },
|
| 1252 |
-
{ url = "https://files.pythonhosted.org/packages/13/f2/9e01678fedc772e09672ed99930de7355757035780d65d59266fcee212b8/libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f", size = 2225834, upload-time = "2025-11-03T22:32:42.716Z" },
|
| 1253 |
-
{ url = "https://files.pythonhosted.org/packages/4a/0d/7bed847b5c8c365e9f1953da274edc87577042bee5a5af21fba63276e756/libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93", size = 2287107, upload-time = "2025-11-03T22:32:44.549Z" },
|
| 1254 |
-
{ url = "https://files.pythonhosted.org/packages/02/f0/7e51fa84ade26c518bfbe7e2e4758b56d86a114c72d60309ac0d350426c4/libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012", size = 2288672, upload-time = "2025-11-03T22:32:45.867Z" },
|
| 1255 |
-
{ url = "https://files.pythonhosted.org/packages/ad/cd/15762659a3f5799d36aab1bc2b7e732672722e249d7800e3c5f943b41250/libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4", size = 2392661, upload-time = "2025-11-03T22:32:47.232Z" },
|
| 1256 |
-
{ url = "https://files.pythonhosted.org/packages/e4/6b/b7f9246c323910fcbe021241500f82e357521495dcfe419004dbb272c7cb/libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330", size = 2105068, upload-time = "2025-11-03T22:32:49.145Z" },
|
| 1257 |
-
{ url = "https://files.pythonhosted.org/packages/a6/0b/4fd40607bc4807ec2b93b054594373d7fa3d31bb983789901afcb9bcebe9/libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42", size = 1985181, upload-time = "2025-11-03T22:32:50.597Z" },
|
| 1258 |
-
{ url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" },
|
| 1259 |
-
{ url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" },
|
| 1260 |
-
{ url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" },
|
| 1261 |
-
{ url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" },
|
| 1262 |
-
{ url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" },
|
| 1263 |
-
{ url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" },
|
| 1264 |
-
{ url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" },
|
| 1265 |
-
{ url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" },
|
| 1266 |
-
{ url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" },
|
| 1267 |
-
{ url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" },
|
| 1268 |
-
{ url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" },
|
| 1269 |
-
{ url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" },
|
| 1270 |
-
{ url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" },
|
| 1271 |
-
{ url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" },
|
| 1272 |
-
{ url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" },
|
| 1273 |
-
{ url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" },
|
| 1274 |
-
]
|
| 1275 |
-
|
| 1276 |
[[package]]
|
| 1277 |
name = "librt"
|
| 1278 |
version = "0.7.8"
|
|
@@ -1325,40 +1188,6 @@ wheels = [
|
|
| 1325 |
{ url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
|
| 1326 |
]
|
| 1327 |
|
| 1328 |
-
[[package]]
|
| 1329 |
-
name = "lightning"
|
| 1330 |
-
version = "2.6.0"
|
| 1331 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1332 |
-
dependencies = [
|
| 1333 |
-
{ name = "fsspec", extra = ["http"] },
|
| 1334 |
-
{ name = "lightning-utilities" },
|
| 1335 |
-
{ name = "packaging" },
|
| 1336 |
-
{ name = "pytorch-lightning" },
|
| 1337 |
-
{ name = "pyyaml" },
|
| 1338 |
-
{ name = "torch" },
|
| 1339 |
-
{ name = "torchmetrics" },
|
| 1340 |
-
{ name = "tqdm" },
|
| 1341 |
-
{ name = "typing-extensions" },
|
| 1342 |
-
]
|
| 1343 |
-
sdist = { url = "https://files.pythonhosted.org/packages/1a/d5/892ea38816925b3511493e87b0b32494122bf8a20e66f4f2cd2667f95625/lightning-2.6.0.tar.gz", hash = "sha256:881841716b59c1837ae0c562c2e64fea9bcf49ef9de3867bd1f868557ec23d04", size = 656539, upload-time = "2025-11-28T09:34:25.069Z" }
|
| 1344 |
-
wheels = [
|
| 1345 |
-
{ url = "https://files.pythonhosted.org/packages/d6/e9/36b340c7ec01dad6f034481e98fc9fc0133307beb05c714c0542af98bbde/lightning-2.6.0-py3-none-any.whl", hash = "sha256:f1a13a48909960a3454518486f113fae4fadb2db0e28e9c50d8d38d46c9dc3d6", size = 845956, upload-time = "2025-11-28T09:34:23.273Z" },
|
| 1346 |
-
]
|
| 1347 |
-
|
| 1348 |
-
[[package]]
|
| 1349 |
-
name = "lightning-utilities"
|
| 1350 |
-
version = "0.15.2"
|
| 1351 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1352 |
-
dependencies = [
|
| 1353 |
-
{ name = "packaging" },
|
| 1354 |
-
{ name = "setuptools" },
|
| 1355 |
-
{ name = "typing-extensions" },
|
| 1356 |
-
]
|
| 1357 |
-
sdist = { url = "https://files.pythonhosted.org/packages/b8/39/6fc58ca81492db047149b4b8fd385aa1bfb8c28cd7cacb0c7eb0c44d842f/lightning_utilities-0.15.2.tar.gz", hash = "sha256:cdf12f530214a63dacefd713f180d1ecf5d165338101617b4742e8f22c032e24", size = 31090, upload-time = "2025-08-06T13:57:39.242Z" }
|
| 1358 |
-
wheels = [
|
| 1359 |
-
{ url = "https://files.pythonhosted.org/packages/de/73/3d757cb3fc16f0f9794dd289bcd0c4a031d9cf54d8137d6b984b2d02edf3/lightning_utilities-0.15.2-py3-none-any.whl", hash = "sha256:ad3ab1703775044bbf880dbf7ddaaac899396c96315f3aa1779cec9d618a9841", size = 29431, upload-time = "2025-08-06T13:57:38.046Z" },
|
| 1360 |
-
]
|
| 1361 |
-
|
| 1362 |
[[package]]
|
| 1363 |
name = "markupsafe"
|
| 1364 |
version = "3.0.3"
|
|
@@ -1422,15 +1251,6 @@ wheels = [
|
|
| 1422 |
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
| 1423 |
]
|
| 1424 |
|
| 1425 |
-
[[package]]
|
| 1426 |
-
name = "mpmath"
|
| 1427 |
-
version = "1.3.0"
|
| 1428 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1429 |
-
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
|
| 1430 |
-
wheels = [
|
| 1431 |
-
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
| 1432 |
-
]
|
| 1433 |
-
|
| 1434 |
[[package]]
|
| 1435 |
name = "multidict"
|
| 1436 |
version = "6.7.0"
|
|
@@ -1572,15 +1392,6 @@ wheels = [
|
|
| 1572 |
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
| 1573 |
]
|
| 1574 |
|
| 1575 |
-
[[package]]
|
| 1576 |
-
name = "networkx"
|
| 1577 |
-
version = "3.6.1"
|
| 1578 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1579 |
-
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
|
| 1580 |
-
wheels = [
|
| 1581 |
-
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
| 1582 |
-
]
|
| 1583 |
-
|
| 1584 |
[[package]]
|
| 1585 |
name = "numpy"
|
| 1586 |
version = "2.4.0"
|
|
@@ -1642,140 +1453,6 @@ wheels = [
|
|
| 1642 |
{ url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" },
|
| 1643 |
]
|
| 1644 |
|
| 1645 |
-
[[package]]
|
| 1646 |
-
name = "nvidia-cublas-cu12"
|
| 1647 |
-
version = "12.8.4.1"
|
| 1648 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1649 |
-
wheels = [
|
| 1650 |
-
{ url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" },
|
| 1651 |
-
]
|
| 1652 |
-
|
| 1653 |
-
[[package]]
|
| 1654 |
-
name = "nvidia-cuda-cupti-cu12"
|
| 1655 |
-
version = "12.8.90"
|
| 1656 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1657 |
-
wheels = [
|
| 1658 |
-
{ url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" },
|
| 1659 |
-
]
|
| 1660 |
-
|
| 1661 |
-
[[package]]
|
| 1662 |
-
name = "nvidia-cuda-nvrtc-cu12"
|
| 1663 |
-
version = "12.8.93"
|
| 1664 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1665 |
-
wheels = [
|
| 1666 |
-
{ url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" },
|
| 1667 |
-
]
|
| 1668 |
-
|
| 1669 |
-
[[package]]
|
| 1670 |
-
name = "nvidia-cuda-runtime-cu12"
|
| 1671 |
-
version = "12.8.90"
|
| 1672 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1673 |
-
wheels = [
|
| 1674 |
-
{ url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" },
|
| 1675 |
-
]
|
| 1676 |
-
|
| 1677 |
-
[[package]]
|
| 1678 |
-
name = "nvidia-cudnn-cu12"
|
| 1679 |
-
version = "9.10.2.21"
|
| 1680 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1681 |
-
dependencies = [
|
| 1682 |
-
{ name = "nvidia-cublas-cu12" },
|
| 1683 |
-
]
|
| 1684 |
-
wheels = [
|
| 1685 |
-
{ url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
|
| 1686 |
-
]
|
| 1687 |
-
|
| 1688 |
-
[[package]]
|
| 1689 |
-
name = "nvidia-cufft-cu12"
|
| 1690 |
-
version = "11.3.3.83"
|
| 1691 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1692 |
-
dependencies = [
|
| 1693 |
-
{ name = "nvidia-nvjitlink-cu12" },
|
| 1694 |
-
]
|
| 1695 |
-
wheels = [
|
| 1696 |
-
{ url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
|
| 1697 |
-
]
|
| 1698 |
-
|
| 1699 |
-
[[package]]
|
| 1700 |
-
name = "nvidia-cufile-cu12"
|
| 1701 |
-
version = "1.13.1.3"
|
| 1702 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1703 |
-
wheels = [
|
| 1704 |
-
{ url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" },
|
| 1705 |
-
]
|
| 1706 |
-
|
| 1707 |
-
[[package]]
|
| 1708 |
-
name = "nvidia-curand-cu12"
|
| 1709 |
-
version = "10.3.9.90"
|
| 1710 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1711 |
-
wheels = [
|
| 1712 |
-
{ url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" },
|
| 1713 |
-
]
|
| 1714 |
-
|
| 1715 |
-
[[package]]
|
| 1716 |
-
name = "nvidia-cusolver-cu12"
|
| 1717 |
-
version = "11.7.3.90"
|
| 1718 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1719 |
-
dependencies = [
|
| 1720 |
-
{ name = "nvidia-cublas-cu12" },
|
| 1721 |
-
{ name = "nvidia-cusparse-cu12" },
|
| 1722 |
-
{ name = "nvidia-nvjitlink-cu12" },
|
| 1723 |
-
]
|
| 1724 |
-
wheels = [
|
| 1725 |
-
{ url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
|
| 1726 |
-
]
|
| 1727 |
-
|
| 1728 |
-
[[package]]
|
| 1729 |
-
name = "nvidia-cusparse-cu12"
|
| 1730 |
-
version = "12.5.8.93"
|
| 1731 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1732 |
-
dependencies = [
|
| 1733 |
-
{ name = "nvidia-nvjitlink-cu12" },
|
| 1734 |
-
]
|
| 1735 |
-
wheels = [
|
| 1736 |
-
{ url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
|
| 1737 |
-
]
|
| 1738 |
-
|
| 1739 |
-
[[package]]
|
| 1740 |
-
name = "nvidia-cusparselt-cu12"
|
| 1741 |
-
version = "0.7.1"
|
| 1742 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1743 |
-
wheels = [
|
| 1744 |
-
{ url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" },
|
| 1745 |
-
]
|
| 1746 |
-
|
| 1747 |
-
[[package]]
|
| 1748 |
-
name = "nvidia-nccl-cu12"
|
| 1749 |
-
version = "2.27.5"
|
| 1750 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1751 |
-
wheels = [
|
| 1752 |
-
{ url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" },
|
| 1753 |
-
]
|
| 1754 |
-
|
| 1755 |
-
[[package]]
|
| 1756 |
-
name = "nvidia-nvjitlink-cu12"
|
| 1757 |
-
version = "12.8.93"
|
| 1758 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1759 |
-
wheels = [
|
| 1760 |
-
{ url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" },
|
| 1761 |
-
]
|
| 1762 |
-
|
| 1763 |
-
[[package]]
|
| 1764 |
-
name = "nvidia-nvshmem-cu12"
|
| 1765 |
-
version = "3.3.20"
|
| 1766 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1767 |
-
wheels = [
|
| 1768 |
-
{ url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" },
|
| 1769 |
-
]
|
| 1770 |
-
|
| 1771 |
-
[[package]]
|
| 1772 |
-
name = "nvidia-nvtx-cu12"
|
| 1773 |
-
version = "12.8.90"
|
| 1774 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1775 |
-
wheels = [
|
| 1776 |
-
{ url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" },
|
| 1777 |
-
]
|
| 1778 |
-
|
| 1779 |
[[package]]
|
| 1780 |
name = "oauthlib"
|
| 1781 |
version = "3.3.1"
|
|
@@ -1785,19 +1462,6 @@ wheels = [
|
|
| 1785 |
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
|
| 1786 |
]
|
| 1787 |
|
| 1788 |
-
[[package]]
|
| 1789 |
-
name = "omegaconf"
|
| 1790 |
-
version = "2.3.0"
|
| 1791 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1792 |
-
dependencies = [
|
| 1793 |
-
{ name = "antlr4-python3-runtime" },
|
| 1794 |
-
{ name = "pyyaml" },
|
| 1795 |
-
]
|
| 1796 |
-
sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" }
|
| 1797 |
-
wheels = [
|
| 1798 |
-
{ url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" },
|
| 1799 |
-
]
|
| 1800 |
-
|
| 1801 |
[[package]]
|
| 1802 |
name = "orjson"
|
| 1803 |
version = "3.11.5"
|
|
@@ -2446,25 +2110,6 @@ wheels = [
|
|
| 2446 |
{ url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" },
|
| 2447 |
]
|
| 2448 |
|
| 2449 |
-
[[package]]
|
| 2450 |
-
name = "pytorch-lightning"
|
| 2451 |
-
version = "2.6.0"
|
| 2452 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2453 |
-
dependencies = [
|
| 2454 |
-
{ name = "fsspec", extra = ["http"] },
|
| 2455 |
-
{ name = "lightning-utilities" },
|
| 2456 |
-
{ name = "packaging" },
|
| 2457 |
-
{ name = "pyyaml" },
|
| 2458 |
-
{ name = "torch" },
|
| 2459 |
-
{ name = "torchmetrics" },
|
| 2460 |
-
{ name = "tqdm" },
|
| 2461 |
-
{ name = "typing-extensions" },
|
| 2462 |
-
]
|
| 2463 |
-
sdist = { url = "https://files.pythonhosted.org/packages/07/d7/e3963d9669758f93b07941f4e2e82a394eb3d0980e29baa4764f3bad6689/pytorch_lightning-2.6.0.tar.gz", hash = "sha256:25b0d4f05e1f33b72be0920c34d0465777fe5f623228f9d6252b4b0f685d7037", size = 658853, upload-time = "2025-11-28T09:34:13.098Z" }
|
| 2464 |
-
wheels = [
|
| 2465 |
-
{ url = "https://files.pythonhosted.org/packages/77/eb/cc6dbfe70d15318dbce82674b1e8057cef2634ca9f9121a16b8a06c630db/pytorch_lightning-2.6.0-py3-none-any.whl", hash = "sha256:ee72cff4b8c983ecfaae8599382544bd5236d9eb300adc7dd305f359195f4e79", size = 849476, upload-time = "2025-11-28T09:34:11.271Z" },
|
| 2466 |
-
]
|
| 2467 |
-
|
| 2468 |
[[package]]
|
| 2469 |
name = "pyyaml"
|
| 2470 |
version = "6.0.3"
|
|
@@ -2511,30 +2156,6 @@ wheels = [
|
|
| 2511 |
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
| 2512 |
]
|
| 2513 |
|
| 2514 |
-
[[package]]
|
| 2515 |
-
name = "pyyaml-ft"
|
| 2516 |
-
version = "8.0.0"
|
| 2517 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2518 |
-
sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" }
|
| 2519 |
-
wheels = [
|
| 2520 |
-
{ url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" },
|
| 2521 |
-
{ url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" },
|
| 2522 |
-
{ url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" },
|
| 2523 |
-
{ url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" },
|
| 2524 |
-
{ url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" },
|
| 2525 |
-
{ url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" },
|
| 2526 |
-
{ url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" },
|
| 2527 |
-
{ url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" },
|
| 2528 |
-
{ url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" },
|
| 2529 |
-
{ url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" },
|
| 2530 |
-
{ url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" },
|
| 2531 |
-
{ url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" },
|
| 2532 |
-
{ url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" },
|
| 2533 |
-
{ url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" },
|
| 2534 |
-
{ url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" },
|
| 2535 |
-
{ url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" },
|
| 2536 |
-
]
|
| 2537 |
-
|
| 2538 |
[[package]]
|
| 2539 |
name = "requests"
|
| 2540 |
version = "2.32.5"
|
|
@@ -2613,15 +2234,6 @@ wheels = [
|
|
| 2613 |
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
|
| 2614 |
]
|
| 2615 |
|
| 2616 |
-
[[package]]
|
| 2617 |
-
name = "setuptools"
|
| 2618 |
-
version = "80.9.0"
|
| 2619 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2620 |
-
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
| 2621 |
-
wheels = [
|
| 2622 |
-
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
| 2623 |
-
]
|
| 2624 |
-
|
| 2625 |
[[package]]
|
| 2626 |
name = "six"
|
| 2627 |
version = "1.17.0"
|
|
@@ -2636,13 +2248,10 @@ name = "specs-before-code-api"
|
|
| 2636 |
version = "1.0.0"
|
| 2637 |
source = { virtual = "." }
|
| 2638 |
dependencies = [
|
| 2639 |
-
{ name = "cloudpickle" },
|
| 2640 |
{ name = "fastapi" },
|
| 2641 |
-
{ name = "fiddle" },
|
| 2642 |
{ name = "fpdf2" },
|
| 2643 |
{ name = "google-api-python-client" },
|
| 2644 |
{ name = "google-auth-oauthlib" },
|
| 2645 |
-
{ name = "hydra-core" },
|
| 2646 |
{ name = "jinja2" },
|
| 2647 |
{ name = "langchain-core" },
|
| 2648 |
{ name = "langchain-mongodb" },
|
|
@@ -2650,7 +2259,6 @@ dependencies = [
|
|
| 2650 |
{ name = "langchain-text-splitters" },
|
| 2651 |
{ name = "langgraph" },
|
| 2652 |
{ name = "langsmith" },
|
| 2653 |
-
{ name = "lightning" },
|
| 2654 |
{ name = "numpy" },
|
| 2655 |
{ name = "passlib", extra = ["bcrypt"] },
|
| 2656 |
{ name = "psycopg2-binary" },
|
|
@@ -2689,14 +2297,11 @@ dev = [
|
|
| 2689 |
[package.metadata]
|
| 2690 |
requires-dist = [
|
| 2691 |
{ name = "black", marker = "extra == 'dev'", specifier = ">=24.0.0" },
|
| 2692 |
-
{ name = "cloudpickle", specifier = ">=3.1.2" },
|
| 2693 |
{ name = "fastapi", specifier = ">=0.123.0" },
|
| 2694 |
-
{ name = "fiddle", specifier = ">=0.3.0" },
|
| 2695 |
{ name = "fpdf2", specifier = ">=2.8.0" },
|
| 2696 |
{ name = "google-api-python-client", specifier = ">=2.111.0" },
|
| 2697 |
{ name = "google-auth-oauthlib", specifier = ">=1.2.0" },
|
| 2698 |
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" },
|
| 2699 |
-
{ name = "hydra-core", specifier = ">=1.3.2" },
|
| 2700 |
{ name = "jinja2", specifier = ">=3.1.6" },
|
| 2701 |
{ name = "langchain-core", specifier = ">=0.1.0" },
|
| 2702 |
{ name = "langchain-mongodb", specifier = ">=0.1.0" },
|
|
@@ -2704,7 +2309,6 @@ requires-dist = [
|
|
| 2704 |
{ name = "langchain-text-splitters", specifier = ">=0.0.1" },
|
| 2705 |
{ name = "langgraph", specifier = ">=0.2.39" },
|
| 2706 |
{ name = "langsmith", specifier = ">=0.1.0" },
|
| 2707 |
-
{ name = "lightning", specifier = ">=2.6.0" },
|
| 2708 |
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" },
|
| 2709 |
{ name = "numpy", specifier = ">=1.26.0" },
|
| 2710 |
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
|
@@ -2783,18 +2387,6 @@ wheels = [
|
|
| 2783 |
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
|
| 2784 |
]
|
| 2785 |
|
| 2786 |
-
[[package]]
|
| 2787 |
-
name = "sympy"
|
| 2788 |
-
version = "1.14.0"
|
| 2789 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2790 |
-
dependencies = [
|
| 2791 |
-
{ name = "mpmath" },
|
| 2792 |
-
]
|
| 2793 |
-
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
| 2794 |
-
wheels = [
|
| 2795 |
-
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
|
| 2796 |
-
]
|
| 2797 |
-
|
| 2798 |
[[package]]
|
| 2799 |
name = "tenacity"
|
| 2800 |
version = "9.1.2"
|
|
@@ -2804,97 +2396,6 @@ wheels = [
|
|
| 2804 |
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
|
| 2805 |
]
|
| 2806 |
|
| 2807 |
-
[[package]]
|
| 2808 |
-
name = "torch"
|
| 2809 |
-
version = "2.9.1"
|
| 2810 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2811 |
-
dependencies = [
|
| 2812 |
-
{ name = "filelock" },
|
| 2813 |
-
{ name = "fsspec" },
|
| 2814 |
-
{ name = "jinja2" },
|
| 2815 |
-
{ name = "networkx" },
|
| 2816 |
-
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2817 |
-
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2818 |
-
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2819 |
-
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2820 |
-
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2821 |
-
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2822 |
-
{ name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2823 |
-
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2824 |
-
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2825 |
-
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2826 |
-
{ name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2827 |
-
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2828 |
-
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2829 |
-
{ name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2830 |
-
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2831 |
-
{ name = "setuptools" },
|
| 2832 |
-
{ name = "sympy" },
|
| 2833 |
-
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
| 2834 |
-
{ name = "typing-extensions" },
|
| 2835 |
-
]
|
| 2836 |
-
wheels = [
|
| 2837 |
-
{ url = "https://files.pythonhosted.org/packages/0f/27/07c645c7673e73e53ded71705045d6cb5bae94c4b021b03aa8d03eee90ab/torch-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6", size = 104126592, upload-time = "2025-11-12T15:20:41.62Z" },
|
| 2838 |
-
{ url = "https://files.pythonhosted.org/packages/19/17/e377a460603132b00760511299fceba4102bd95db1a0ee788da21298ccff/torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4", size = 899742281, upload-time = "2025-11-12T15:22:17.602Z" },
|
| 2839 |
-
{ url = "https://files.pythonhosted.org/packages/b1/1a/64f5769025db846a82567fa5b7d21dba4558a7234ee631712ee4771c436c/torch-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083", size = 110940568, upload-time = "2025-11-12T15:21:18.689Z" },
|
| 2840 |
-
{ url = "https://files.pythonhosted.org/packages/6e/ab/07739fd776618e5882661d04c43f5b5586323e2f6a2d7d84aac20d8f20bd/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e", size = 74479191, upload-time = "2025-11-12T15:21:25.816Z" },
|
| 2841 |
-
{ url = "https://files.pythonhosted.org/packages/20/60/8fc5e828d050bddfab469b3fe78e5ab9a7e53dda9c3bdc6a43d17ce99e63/torch-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb", size = 104135743, upload-time = "2025-11-12T15:21:34.936Z" },
|
| 2842 |
-
{ url = "https://files.pythonhosted.org/packages/f2/b7/6d3f80e6918213babddb2a37b46dbb14c15b14c5f473e347869a51f40e1f/torch-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9", size = 899749493, upload-time = "2025-11-12T15:24:36.356Z" },
|
| 2843 |
-
{ url = "https://files.pythonhosted.org/packages/a6/47/c7843d69d6de8938c1cbb1eba426b1d48ddf375f101473d3e31a5fc52b74/torch-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2", size = 110944162, upload-time = "2025-11-12T15:21:53.151Z" },
|
| 2844 |
-
{ url = "https://files.pythonhosted.org/packages/28/0e/2a37247957e72c12151b33a01e4df651d9d155dd74d8cfcbfad15a79b44a/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e", size = 74830751, upload-time = "2025-11-12T15:21:43.792Z" },
|
| 2845 |
-
{ url = "https://files.pythonhosted.org/packages/4b/f7/7a18745edcd7b9ca2381aa03353647bca8aace91683c4975f19ac233809d/torch-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a", size = 104142929, upload-time = "2025-11-12T15:21:48.319Z" },
|
| 2846 |
-
{ url = "https://files.pythonhosted.org/packages/f4/dd/f1c0d879f2863ef209e18823a988dc7a1bf40470750e3ebe927efdb9407f/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978, upload-time = "2025-11-12T15:23:04.568Z" },
|
| 2847 |
-
{ url = "https://files.pythonhosted.org/packages/1f/9f/6986b83a53b4d043e36f3f898b798ab51f7f20fdf1a9b01a2720f445043d/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995, upload-time = "2025-11-12T15:22:01.618Z" },
|
| 2848 |
-
{ url = "https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347, upload-time = "2025-11-12T15:21:57.648Z" },
|
| 2849 |
-
{ url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245, upload-time = "2025-11-12T15:22:39.027Z" },
|
| 2850 |
-
{ url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804, upload-time = "2025-11-12T15:22:35.222Z" },
|
| 2851 |
-
{ url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132, upload-time = "2025-11-12T15:23:36.068Z" },
|
| 2852 |
-
{ url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845, upload-time = "2025-11-12T15:22:48.367Z" },
|
| 2853 |
-
{ url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558, upload-time = "2025-11-12T15:22:43.392Z" },
|
| 2854 |
-
{ url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788, upload-time = "2025-11-12T15:23:52.109Z" },
|
| 2855 |
-
{ url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500, upload-time = "2025-11-12T15:24:08.788Z" },
|
| 2856 |
-
{ url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" },
|
| 2857 |
-
]
|
| 2858 |
-
|
| 2859 |
-
[[package]]
|
| 2860 |
-
name = "torchmetrics"
|
| 2861 |
-
version = "1.8.2"
|
| 2862 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2863 |
-
dependencies = [
|
| 2864 |
-
{ name = "lightning-utilities" },
|
| 2865 |
-
{ name = "numpy" },
|
| 2866 |
-
{ name = "packaging" },
|
| 2867 |
-
{ name = "torch" },
|
| 2868 |
-
]
|
| 2869 |
-
sdist = { url = "https://files.pythonhosted.org/packages/85/2e/48a887a59ecc4a10ce9e8b35b3e3c5cef29d902c4eac143378526e7485cb/torchmetrics-1.8.2.tar.gz", hash = "sha256:cf64a901036bf107f17a524009eea7781c9c5315d130713aeca5747a686fe7a5", size = 580679, upload-time = "2025-09-03T14:00:54.077Z" }
|
| 2870 |
-
wheels = [
|
| 2871 |
-
{ url = "https://files.pythonhosted.org/packages/02/21/aa0f434434c48490f91b65962b1ce863fdcce63febc166ca9fe9d706c2b6/torchmetrics-1.8.2-py3-none-any.whl", hash = "sha256:08382fd96b923e39e904c4d570f3d49e2cc71ccabd2a94e0f895d1f0dac86242", size = 983161, upload-time = "2025-09-03T14:00:51.921Z" },
|
| 2872 |
-
]
|
| 2873 |
-
|
| 2874 |
-
[[package]]
|
| 2875 |
-
name = "tqdm"
|
| 2876 |
-
version = "4.67.1"
|
| 2877 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2878 |
-
dependencies = [
|
| 2879 |
-
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 2880 |
-
]
|
| 2881 |
-
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
| 2882 |
-
wheels = [
|
| 2883 |
-
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
| 2884 |
-
]
|
| 2885 |
-
|
| 2886 |
-
[[package]]
|
| 2887 |
-
name = "triton"
|
| 2888 |
-
version = "3.5.1"
|
| 2889 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2890 |
-
wheels = [
|
| 2891 |
-
{ url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207, upload-time = "2025-11-11T17:41:00.253Z" },
|
| 2892 |
-
{ url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410, upload-time = "2025-11-11T17:41:06.319Z" },
|
| 2893 |
-
{ url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924, upload-time = "2025-11-11T17:41:12.455Z" },
|
| 2894 |
-
{ url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488, upload-time = "2025-11-11T17:41:18.222Z" },
|
| 2895 |
-
{ url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192, upload-time = "2025-11-11T17:41:23.963Z" },
|
| 2896 |
-
]
|
| 2897 |
-
|
| 2898 |
[[package]]
|
| 2899 |
name = "typing-extensions"
|
| 2900 |
version = "4.15.0"
|
|
|
|
| 7 |
"python_full_version < '3.13'",
|
| 8 |
]
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
[[package]]
|
| 11 |
name = "aiohappyeyeballs"
|
| 12 |
version = "2.6.1"
|
|
|
|
| 132 |
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
| 133 |
]
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
[[package]]
|
| 136 |
name = "anyio"
|
| 137 |
version = "4.12.0"
|
|
|
|
| 396 |
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
| 397 |
]
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
[[package]]
|
| 400 |
name = "colorama"
|
| 401 |
version = "0.4.6"
|
|
|
|
| 580 |
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
| 581 |
]
|
| 582 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
[[package]]
|
| 584 |
name = "filetype"
|
| 585 |
version = "1.2.0"
|
|
|
|
| 733 |
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
| 734 |
]
|
| 735 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
[[package]]
|
| 737 |
name = "google-api-core"
|
| 738 |
version = "2.28.1"
|
|
|
|
| 817 |
{ url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
|
| 818 |
]
|
| 819 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
[[package]]
|
| 821 |
name = "greenlet"
|
| 822 |
version = "3.3.0"
|
|
|
|
| 905 |
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
| 906 |
]
|
| 907 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 908 |
[[package]]
|
| 909 |
name = "idna"
|
| 910 |
version = "3.11"
|
|
|
|
| 1136 |
{ url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" },
|
| 1137 |
]
|
| 1138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1139 |
[[package]]
|
| 1140 |
name = "librt"
|
| 1141 |
version = "0.7.8"
|
|
|
|
| 1188 |
{ url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
|
| 1189 |
]
|
| 1190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1191 |
[[package]]
|
| 1192 |
name = "markupsafe"
|
| 1193 |
version = "3.0.3"
|
|
|
|
| 1251 |
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
| 1252 |
]
|
| 1253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1254 |
[[package]]
|
| 1255 |
name = "multidict"
|
| 1256 |
version = "6.7.0"
|
|
|
|
| 1392 |
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
| 1393 |
]
|
| 1394 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1395 |
[[package]]
|
| 1396 |
name = "numpy"
|
| 1397 |
version = "2.4.0"
|
|
|
|
| 1453 |
{ url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" },
|
| 1454 |
]
|
| 1455 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1456 |
[[package]]
|
| 1457 |
name = "oauthlib"
|
| 1458 |
version = "3.3.1"
|
|
|
|
| 1462 |
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
|
| 1463 |
]
|
| 1464 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1465 |
[[package]]
|
| 1466 |
name = "orjson"
|
| 1467 |
version = "3.11.5"
|
|
|
|
| 2110 |
{ url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" },
|
| 2111 |
]
|
| 2112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2113 |
[[package]]
|
| 2114 |
name = "pyyaml"
|
| 2115 |
version = "6.0.3"
|
|
|
|
| 2156 |
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
| 2157 |
]
|
| 2158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2159 |
[[package]]
|
| 2160 |
name = "requests"
|
| 2161 |
version = "2.32.5"
|
|
|
|
| 2234 |
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
|
| 2235 |
]
|
| 2236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2237 |
[[package]]
|
| 2238 |
name = "six"
|
| 2239 |
version = "1.17.0"
|
|
|
|
| 2248 |
version = "1.0.0"
|
| 2249 |
source = { virtual = "." }
|
| 2250 |
dependencies = [
|
|
|
|
| 2251 |
{ name = "fastapi" },
|
|
|
|
| 2252 |
{ name = "fpdf2" },
|
| 2253 |
{ name = "google-api-python-client" },
|
| 2254 |
{ name = "google-auth-oauthlib" },
|
|
|
|
| 2255 |
{ name = "jinja2" },
|
| 2256 |
{ name = "langchain-core" },
|
| 2257 |
{ name = "langchain-mongodb" },
|
|
|
|
| 2259 |
{ name = "langchain-text-splitters" },
|
| 2260 |
{ name = "langgraph" },
|
| 2261 |
{ name = "langsmith" },
|
|
|
|
| 2262 |
{ name = "numpy" },
|
| 2263 |
{ name = "passlib", extra = ["bcrypt"] },
|
| 2264 |
{ name = "psycopg2-binary" },
|
|
|
|
| 2297 |
[package.metadata]
|
| 2298 |
requires-dist = [
|
| 2299 |
{ name = "black", marker = "extra == 'dev'", specifier = ">=24.0.0" },
|
|
|
|
| 2300 |
{ name = "fastapi", specifier = ">=0.123.0" },
|
|
|
|
| 2301 |
{ name = "fpdf2", specifier = ">=2.8.0" },
|
| 2302 |
{ name = "google-api-python-client", specifier = ">=2.111.0" },
|
| 2303 |
{ name = "google-auth-oauthlib", specifier = ">=1.2.0" },
|
| 2304 |
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" },
|
|
|
|
| 2305 |
{ name = "jinja2", specifier = ">=3.1.6" },
|
| 2306 |
{ name = "langchain-core", specifier = ">=0.1.0" },
|
| 2307 |
{ name = "langchain-mongodb", specifier = ">=0.1.0" },
|
|
|
|
| 2309 |
{ name = "langchain-text-splitters", specifier = ">=0.0.1" },
|
| 2310 |
{ name = "langgraph", specifier = ">=0.2.39" },
|
| 2311 |
{ name = "langsmith", specifier = ">=0.1.0" },
|
|
|
|
| 2312 |
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" },
|
| 2313 |
{ name = "numpy", specifier = ">=1.26.0" },
|
| 2314 |
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
|
|
|
| 2387 |
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
|
| 2388 |
]
|
| 2389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2390 |
[[package]]
|
| 2391 |
name = "tenacity"
|
| 2392 |
version = "9.1.2"
|
|
|
|
| 2396 |
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
|
| 2397 |
]
|
| 2398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2399 |
[[package]]
|
| 2400 |
name = "typing-extensions"
|
| 2401 |
version = "4.15.0"
|