ouroboros-ai 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/bigbang/ambiguity.py +39 -62
- ouroboros/bigbang/interview.py +16 -18
- ouroboros/cli/commands/init.py +162 -97
- {ouroboros_ai-0.2.2.dist-info → ouroboros_ai-0.3.0.dist-info}/METADATA +65 -14
- {ouroboros_ai-0.2.2.dist-info → ouroboros_ai-0.3.0.dist-info}/RECORD +8 -8
- {ouroboros_ai-0.2.2.dist-info → ouroboros_ai-0.3.0.dist-info}/WHEEL +0 -0
- {ouroboros_ai-0.2.2.dist-info → ouroboros_ai-0.3.0.dist-info}/entry_points.txt +0 -0
- {ouroboros_ai-0.2.2.dist-info → ouroboros_ai-0.3.0.dist-info}/licenses/LICENSE +0 -0
ouroboros/bigbang/ambiguity.py
CHANGED
|
@@ -9,6 +9,8 @@ The scoring algorithm evaluates three key components:
|
|
|
9
9
|
- Success Criteria Clarity (30%): How measurable the success criteria are
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
12
14
|
from dataclasses import dataclass
|
|
13
15
|
from typing import Any
|
|
14
16
|
|
|
@@ -139,6 +141,7 @@ class AmbiguityScorer:
|
|
|
139
141
|
temperature: float = SCORING_TEMPERATURE
|
|
140
142
|
initial_max_tokens: int = 2048
|
|
141
143
|
max_retries: int | None = None # None = unlimited retries
|
|
144
|
+
max_format_error_retries: int = 5 # Stop after N format errors (non-truncation)
|
|
142
145
|
|
|
143
146
|
async def score(
|
|
144
147
|
self, state: InterviewState
|
|
@@ -303,38 +306,19 @@ class AmbiguityScorer:
|
|
|
303
306
|
Returns:
|
|
304
307
|
System prompt string.
|
|
305
308
|
"""
|
|
306
|
-
return """You are an expert requirements analyst
|
|
307
|
-
|
|
308
|
-
Your task is to assess how clear and unambiguous the requirements are based on an interview conversation.
|
|
309
|
+
return """You are an expert requirements analyst. Evaluate the clarity of software requirements.
|
|
309
310
|
|
|
310
311
|
Evaluate three components:
|
|
311
|
-
1. Goal Clarity (40%
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
2. Constraint Clarity (30% weight): Are constraints and limitations specified?
|
|
316
|
-
- Clear: "Must use Python 3.14+, no external database dependencies"
|
|
317
|
-
- Unclear: No mention of technical constraints or limitations
|
|
318
|
-
|
|
319
|
-
3. Success Criteria Clarity (30% weight): Are success criteria measurable?
|
|
320
|
-
- Clear: "Tasks can be created, edited, deleted; supports filtering by status"
|
|
321
|
-
- Unclear: "The tool should be easy to use"
|
|
322
|
-
|
|
323
|
-
For each component, provide:
|
|
324
|
-
- A clarity score between 0.0 (completely unclear) and 1.0 (perfectly clear)
|
|
325
|
-
- A brief justification (1-2 sentences max) explaining the score
|
|
312
|
+
1. Goal Clarity (40%): Is the goal specific and well-defined?
|
|
313
|
+
2. Constraint Clarity (30%): Are constraints and limitations specified?
|
|
314
|
+
3. Success Criteria Clarity (30%): Are success criteria measurable?
|
|
326
315
|
|
|
327
|
-
|
|
316
|
+
Score each from 0.0 (unclear) to 1.0 (perfectly clear). Scores above 0.8 require very specific requirements.
|
|
328
317
|
|
|
329
|
-
|
|
330
|
-
GOAL_CLARITY_SCORE: <score>
|
|
331
|
-
GOAL_CLARITY_JUSTIFICATION: <justification in 1-2 sentences>
|
|
332
|
-
CONSTRAINT_CLARITY_SCORE: <score>
|
|
333
|
-
CONSTRAINT_CLARITY_JUSTIFICATION: <justification in 1-2 sentences>
|
|
334
|
-
SUCCESS_CRITERIA_CLARITY_SCORE: <score>
|
|
335
|
-
SUCCESS_CRITERIA_CLARITY_JUSTIFICATION: <justification in 1-2 sentences>
|
|
318
|
+
RESPOND ONLY WITH VALID JSON. No other text before or after.
|
|
336
319
|
|
|
337
|
-
|
|
320
|
+
Required JSON format:
|
|
321
|
+
{"goal_clarity_score": 0.0, "goal_clarity_justification": "string", "constraint_clarity_score": 0.0, "constraint_clarity_justification": "string", "success_criteria_clarity_score": 0.0, "success_criteria_clarity_justification": "string"}"""
|
|
338
322
|
|
|
339
323
|
def _build_scoring_user_prompt(self, context: str) -> str:
|
|
340
324
|
"""Build user prompt with interview context.
|
|
@@ -365,27 +349,23 @@ Analyze each component and provide scores with justifications."""
|
|
|
365
349
|
Raises:
|
|
366
350
|
ValueError: If response cannot be parsed.
|
|
367
351
|
"""
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
key = prefix[:-1].lower() # Remove colon and lowercase
|
|
386
|
-
value = line[len(prefix) :].strip()
|
|
387
|
-
scores[key] = value
|
|
388
|
-
break
|
|
352
|
+
# Extract JSON from response (handle markdown code blocks)
|
|
353
|
+
text = response.strip()
|
|
354
|
+
|
|
355
|
+
# Try to find JSON in markdown code block
|
|
356
|
+
json_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
|
|
357
|
+
if json_match:
|
|
358
|
+
text = json_match.group(1)
|
|
359
|
+
else:
|
|
360
|
+
# Try to find raw JSON object
|
|
361
|
+
json_match = re.search(r"\{.*\}", text, re.DOTALL)
|
|
362
|
+
if json_match:
|
|
363
|
+
text = json_match.group(0)
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
data = json.loads(text)
|
|
367
|
+
except json.JSONDecodeError as e:
|
|
368
|
+
raise ValueError(f"Invalid JSON response: {e}") from e
|
|
389
369
|
|
|
390
370
|
# Validate all required fields are present
|
|
391
371
|
required_fields = [
|
|
@@ -398,35 +378,32 @@ Analyze each component and provide scores with justifications."""
|
|
|
398
378
|
]
|
|
399
379
|
|
|
400
380
|
for field_name in required_fields:
|
|
401
|
-
if field_name not in
|
|
381
|
+
if field_name not in data:
|
|
402
382
|
raise ValueError(f"Missing required field: {field_name}")
|
|
403
383
|
|
|
404
|
-
# Parse
|
|
405
|
-
def
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return max(0.0, min(1.0, score)) # Clamp to [0, 1]
|
|
409
|
-
except ValueError as e:
|
|
410
|
-
raise ValueError(f"Invalid score value: {value}") from e
|
|
384
|
+
# Parse and clamp scores
|
|
385
|
+
def clamp_score(value: Any) -> float:
|
|
386
|
+
score = float(value)
|
|
387
|
+
return max(0.0, min(1.0, score))
|
|
411
388
|
|
|
412
389
|
return ScoreBreakdown(
|
|
413
390
|
goal_clarity=ComponentScore(
|
|
414
391
|
name="Goal Clarity",
|
|
415
|
-
clarity_score=
|
|
392
|
+
clarity_score=clamp_score(data["goal_clarity_score"]),
|
|
416
393
|
weight=GOAL_CLARITY_WEIGHT,
|
|
417
|
-
justification=
|
|
394
|
+
justification=str(data["goal_clarity_justification"]),
|
|
418
395
|
),
|
|
419
396
|
constraint_clarity=ComponentScore(
|
|
420
397
|
name="Constraint Clarity",
|
|
421
|
-
clarity_score=
|
|
398
|
+
clarity_score=clamp_score(data["constraint_clarity_score"]),
|
|
422
399
|
weight=CONSTRAINT_CLARITY_WEIGHT,
|
|
423
|
-
justification=
|
|
400
|
+
justification=str(data["constraint_clarity_justification"]),
|
|
424
401
|
),
|
|
425
402
|
success_criteria_clarity=ComponentScore(
|
|
426
403
|
name="Success Criteria Clarity",
|
|
427
|
-
clarity_score=
|
|
404
|
+
clarity_score=clamp_score(data["success_criteria_clarity_score"]),
|
|
428
405
|
weight=SUCCESS_CRITERIA_CLARITY_WEIGHT,
|
|
429
|
-
justification=
|
|
406
|
+
justification=str(data["success_criteria_clarity_justification"]),
|
|
430
407
|
),
|
|
431
408
|
)
|
|
432
409
|
|
ouroboros/bigbang/interview.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Interactive interview engine for requirement clarification.
|
|
2
2
|
|
|
3
3
|
This module implements the interview protocol that refines vague ideas into
|
|
4
|
-
clear requirements through iterative questioning
|
|
4
|
+
clear requirements through iterative questioning. Users control when to stop.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from collections.abc import Iterator
|
|
@@ -52,10 +52,17 @@ def _file_lock(file_path: Path, exclusive: bool = True) -> Iterator[None]:
|
|
|
52
52
|
|
|
53
53
|
log = structlog.get_logger()
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
# Interview round constants
|
|
56
|
+
MIN_ROUNDS_BEFORE_EARLY_EXIT = 3 # Must complete at least 3 rounds
|
|
57
|
+
SOFT_LIMIT_WARNING_THRESHOLD = 15 # Warn about diminishing returns after this
|
|
58
|
+
DEFAULT_INTERVIEW_ROUNDS = 10 # Reference value for prompts (not enforced)
|
|
59
|
+
|
|
56
60
|
# Default model moved to config.models.ClarificationConfig.default_model
|
|
57
61
|
_FALLBACK_MODEL = "openrouter/google/gemini-2.0-flash-001"
|
|
58
62
|
|
|
63
|
+
# Legacy alias for backward compatibility
|
|
64
|
+
MAX_INTERVIEW_ROUNDS = DEFAULT_INTERVIEW_ROUNDS
|
|
65
|
+
|
|
59
66
|
|
|
60
67
|
class InterviewStatus(StrEnum):
|
|
61
68
|
"""Status of the interview process."""
|
|
@@ -69,13 +76,13 @@ class InterviewRound(BaseModel):
|
|
|
69
76
|
"""A single round of interview questions and responses.
|
|
70
77
|
|
|
71
78
|
Attributes:
|
|
72
|
-
round_number: 1-based round number (
|
|
79
|
+
round_number: 1-based round number (no upper limit - user controls).
|
|
73
80
|
question: The question asked by the system.
|
|
74
81
|
user_response: The user's response (None if not yet answered).
|
|
75
82
|
timestamp: When this round was created.
|
|
76
83
|
"""
|
|
77
84
|
|
|
78
|
-
round_number: int = Field(ge=1
|
|
85
|
+
round_number: int = Field(ge=1) # No upper limit - user decides when to stop
|
|
79
86
|
question: str
|
|
80
87
|
user_response: str | None = None
|
|
81
88
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
@@ -107,11 +114,8 @@ class InterviewState(BaseModel):
|
|
|
107
114
|
|
|
108
115
|
@property
|
|
109
116
|
def is_complete(self) -> bool:
|
|
110
|
-
"""Check if interview
|
|
111
|
-
return
|
|
112
|
-
self.status == InterviewStatus.COMPLETED
|
|
113
|
-
or len(self.rounds) >= MAX_INTERVIEW_ROUNDS
|
|
114
|
-
)
|
|
117
|
+
"""Check if interview is marked complete (user-controlled)."""
|
|
118
|
+
return self.status == InterviewStatus.COMPLETED
|
|
115
119
|
|
|
116
120
|
def mark_updated(self) -> None:
|
|
117
121
|
"""Update the updated_at timestamp."""
|
|
@@ -321,14 +325,8 @@ class InterviewEngine:
|
|
|
321
325
|
response_length=len(user_response),
|
|
322
326
|
)
|
|
323
327
|
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
state.status = InterviewStatus.COMPLETED
|
|
327
|
-
log.info(
|
|
328
|
-
"interview.max_rounds_reached",
|
|
329
|
-
interview_id=state.interview_id,
|
|
330
|
-
total_rounds=len(state.rounds),
|
|
331
|
-
)
|
|
328
|
+
# Note: No auto-complete on round limit. User controls when to stop.
|
|
329
|
+
# CLI handles prompting user to continue after each round.
|
|
332
330
|
|
|
333
331
|
return Result.ok(state)
|
|
334
332
|
|
|
@@ -437,7 +435,7 @@ class InterviewEngine:
|
|
|
437
435
|
Returns:
|
|
438
436
|
The system prompt.
|
|
439
437
|
"""
|
|
440
|
-
round_info = f"Round {state.current_round_number}
|
|
438
|
+
round_info = f"Round {state.current_round_number}"
|
|
441
439
|
|
|
442
440
|
return f"""You are an expert requirements engineer conducting an interview to refine vague ideas into clear, executable requirements.
|
|
443
441
|
|
ouroboros/cli/commands/init.py
CHANGED
|
@@ -5,6 +5,7 @@ Supports both LiteLLM (external API) and Claude Code (Max Plan) modes.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
+
from enum import Enum, auto
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Annotated
|
|
10
11
|
|
|
@@ -12,13 +13,28 @@ from rich.prompt import Confirm, Prompt
|
|
|
12
13
|
import typer
|
|
13
14
|
|
|
14
15
|
from ouroboros.bigbang.ambiguity import AmbiguityScorer
|
|
15
|
-
from ouroboros.bigbang.interview import
|
|
16
|
+
from ouroboros.bigbang.interview import (
|
|
17
|
+
MIN_ROUNDS_BEFORE_EARLY_EXIT,
|
|
18
|
+
SOFT_LIMIT_WARNING_THRESHOLD,
|
|
19
|
+
InterviewEngine,
|
|
20
|
+
InterviewState,
|
|
21
|
+
InterviewStatus,
|
|
22
|
+
)
|
|
16
23
|
from ouroboros.bigbang.seed_generator import SeedGenerator
|
|
17
24
|
from ouroboros.cli.formatters import console
|
|
18
25
|
from ouroboros.cli.formatters.panels import print_error, print_info, print_success, print_warning
|
|
19
26
|
from ouroboros.providers.base import LLMAdapter
|
|
20
27
|
from ouroboros.providers.litellm_adapter import LiteLLMAdapter
|
|
21
28
|
|
|
29
|
+
|
|
30
|
+
class SeedGenerationResult(Enum):
|
|
31
|
+
"""Result of seed generation attempt."""
|
|
32
|
+
|
|
33
|
+
SUCCESS = auto()
|
|
34
|
+
CANCELLED = auto()
|
|
35
|
+
CONTINUE_INTERVIEW = auto()
|
|
36
|
+
|
|
37
|
+
|
|
22
38
|
app = typer.Typer(
|
|
23
39
|
name="init",
|
|
24
40
|
help="Start interactive interview to refine requirements.",
|
|
@@ -43,62 +59,30 @@ def _get_adapter(use_orchestrator: bool) -> LLMAdapter:
|
|
|
43
59
|
return LiteLLMAdapter()
|
|
44
60
|
|
|
45
61
|
|
|
46
|
-
async def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
) -> None:
|
|
52
|
-
"""Run the interview process.
|
|
62
|
+
async def _run_interview_loop(
|
|
63
|
+
engine: InterviewEngine,
|
|
64
|
+
state: InterviewState,
|
|
65
|
+
) -> InterviewState:
|
|
66
|
+
"""Run the interview question loop until completion or user exit.
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
use_orchestrator: If True, use Claude Code (Max Plan) instead of LiteLLM.
|
|
59
|
-
"""
|
|
60
|
-
# Initialize components
|
|
61
|
-
llm_adapter = _get_adapter(use_orchestrator)
|
|
62
|
-
engine = InterviewEngine(
|
|
63
|
-
llm_adapter=llm_adapter,
|
|
64
|
-
state_dir=state_dir or Path.home() / ".ouroboros" / "data",
|
|
65
|
-
)
|
|
68
|
+
Implements tiered confirmation:
|
|
69
|
+
- Rounds 1-3: Auto-continue (minimum context)
|
|
70
|
+
- Rounds 4-15: Ask "Continue?" after each round
|
|
71
|
+
- Rounds 16+: Ask "Continue?" with diminishing returns warning
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
state_result = await engine.load_state(resume_id)
|
|
71
|
-
if state_result.is_err:
|
|
72
|
-
print_error(f"Failed to load interview: {state_result.error.message}")
|
|
73
|
-
raise typer.Exit(code=1)
|
|
74
|
-
state = state_result.value
|
|
75
|
-
else:
|
|
76
|
-
print_info("Starting new interview session...")
|
|
77
|
-
state_result = await engine.start_interview(initial_context)
|
|
78
|
-
if state_result.is_err:
|
|
79
|
-
print_error(f"Failed to start interview: {state_result.error.message}")
|
|
80
|
-
raise typer.Exit(code=1)
|
|
81
|
-
state = state_result.value
|
|
82
|
-
|
|
83
|
-
console.print()
|
|
84
|
-
console.print(
|
|
85
|
-
f"[bold cyan]Interview Session: {state.interview_id}[/]",
|
|
86
|
-
)
|
|
87
|
-
console.print(f"[muted]Max rounds: {MAX_INTERVIEW_ROUNDS}[/]")
|
|
88
|
-
console.print()
|
|
73
|
+
Args:
|
|
74
|
+
engine: Interview engine instance.
|
|
75
|
+
state: Current interview state.
|
|
89
76
|
|
|
90
|
-
|
|
77
|
+
Returns:
|
|
78
|
+
Updated interview state.
|
|
79
|
+
"""
|
|
91
80
|
while not state.is_complete:
|
|
92
81
|
current_round = state.current_round_number
|
|
93
|
-
console.print(
|
|
94
|
-
f"[bold]Round {current_round}/{MAX_INTERVIEW_ROUNDS}[/]",
|
|
95
|
-
)
|
|
82
|
+
console.print(f"[bold]Round {current_round}[/]")
|
|
96
83
|
|
|
97
84
|
# Generate question
|
|
98
|
-
with console.status(
|
|
99
|
-
"[cyan]Generating question...[/]",
|
|
100
|
-
spinner="dots",
|
|
101
|
-
):
|
|
85
|
+
with console.status("[cyan]Generating question...[/]", spinner="dots"):
|
|
102
86
|
question_result = await engine.ask_next_question(state)
|
|
103
87
|
|
|
104
88
|
if question_result.is_err:
|
|
@@ -130,15 +114,22 @@ async def _run_interview(
|
|
|
130
114
|
|
|
131
115
|
state = record_result.value
|
|
132
116
|
|
|
133
|
-
# Save state
|
|
117
|
+
# Save state immediately after recording
|
|
134
118
|
save_result = await engine.save_state(state)
|
|
135
119
|
if save_result.is_err:
|
|
136
120
|
print_error(f"Warning: Failed to save state: {save_result.error.message}")
|
|
137
121
|
|
|
138
122
|
console.print()
|
|
139
123
|
|
|
140
|
-
#
|
|
141
|
-
if
|
|
124
|
+
# Tiered confirmation logic
|
|
125
|
+
if current_round >= MIN_ROUNDS_BEFORE_EARLY_EXIT:
|
|
126
|
+
# Show warning for rounds beyond soft limit
|
|
127
|
+
if current_round >= SOFT_LIMIT_WARNING_THRESHOLD:
|
|
128
|
+
print_warning(
|
|
129
|
+
f"Round {current_round}: Diminishing returns expected. "
|
|
130
|
+
"Consider generating Seed to check ambiguity score."
|
|
131
|
+
)
|
|
132
|
+
|
|
142
133
|
should_continue = Confirm.ask(
|
|
143
134
|
"Continue with more questions?",
|
|
144
135
|
default=True,
|
|
@@ -147,40 +138,104 @@ async def _run_interview(
|
|
|
147
138
|
complete_result = await engine.complete_interview(state)
|
|
148
139
|
if complete_result.is_ok:
|
|
149
140
|
state = complete_result.value
|
|
150
|
-
|
|
141
|
+
await engine.save_state(state)
|
|
151
142
|
break
|
|
152
143
|
|
|
153
|
-
|
|
154
|
-
console.print()
|
|
155
|
-
print_success("Interview completed!")
|
|
156
|
-
console.print(f"[muted]Total rounds: {len(state.rounds)}[/]")
|
|
157
|
-
console.print(f"[muted]Interview ID: {state.interview_id}[/]")
|
|
144
|
+
return state
|
|
158
145
|
|
|
159
|
-
# Save final state
|
|
160
|
-
save_result = await engine.save_state(state)
|
|
161
|
-
if save_result.is_ok:
|
|
162
|
-
console.print(f"[muted]State saved to: {save_result.value}[/]")
|
|
163
146
|
|
|
164
|
-
|
|
147
|
+
async def _run_interview(
|
|
148
|
+
initial_context: str,
|
|
149
|
+
resume_id: str | None = None,
|
|
150
|
+
state_dir: Path | None = None,
|
|
151
|
+
use_orchestrator: bool = False,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Run the interview process.
|
|
165
154
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
155
|
+
Args:
|
|
156
|
+
initial_context: Initial context or idea for the interview.
|
|
157
|
+
resume_id: Optional interview ID to resume.
|
|
158
|
+
state_dir: Optional custom state directory.
|
|
159
|
+
use_orchestrator: If True, use Claude Code (Max Plan) instead of LiteLLM.
|
|
160
|
+
"""
|
|
161
|
+
# Initialize components
|
|
162
|
+
llm_adapter = _get_adapter(use_orchestrator)
|
|
163
|
+
engine = InterviewEngine(
|
|
164
|
+
llm_adapter=llm_adapter,
|
|
165
|
+
state_dir=state_dir or Path.home() / ".ouroboros" / "data",
|
|
170
166
|
)
|
|
171
167
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
# Load or start interview
|
|
169
|
+
if resume_id:
|
|
170
|
+
print_info(f"Resuming interview: {resume_id}")
|
|
171
|
+
state_result = await engine.load_state(resume_id)
|
|
172
|
+
if state_result.is_err:
|
|
173
|
+
print_error(f"Failed to load interview: {state_result.error.message}")
|
|
174
|
+
raise typer.Exit(code=1)
|
|
175
|
+
state = state_result.value
|
|
176
|
+
else:
|
|
177
|
+
print_info("Starting new interview session...")
|
|
178
|
+
state_result = await engine.start_interview(initial_context)
|
|
179
|
+
if state_result.is_err:
|
|
180
|
+
print_error(f"Failed to start interview: {state_result.error.message}")
|
|
181
|
+
raise typer.Exit(code=1)
|
|
182
|
+
state = state_result.value
|
|
183
|
+
|
|
184
|
+
console.print()
|
|
185
|
+
console.print(f"[bold cyan]Interview Session: {state.interview_id}[/]")
|
|
186
|
+
console.print("[muted]No round limit - you decide when to stop[/]")
|
|
187
|
+
console.print()
|
|
188
|
+
|
|
189
|
+
# Run initial interview loop
|
|
190
|
+
state = await _run_interview_loop(engine, state)
|
|
191
|
+
|
|
192
|
+
# Outer loop for retry on high ambiguity
|
|
193
|
+
while True:
|
|
194
|
+
# Interview complete
|
|
195
|
+
console.print()
|
|
196
|
+
print_success("Interview completed!")
|
|
197
|
+
console.print(f"[muted]Total rounds: {len(state.rounds)}[/]")
|
|
198
|
+
console.print(f"[muted]Interview ID: {state.interview_id}[/]")
|
|
199
|
+
|
|
200
|
+
# Save final state
|
|
201
|
+
save_result = await engine.save_state(state)
|
|
202
|
+
if save_result.is_ok:
|
|
203
|
+
console.print(f"[muted]State saved to: {save_result.value}[/]")
|
|
204
|
+
|
|
205
|
+
console.print()
|
|
206
|
+
|
|
207
|
+
# Ask if user wants to proceed to Seed generation
|
|
208
|
+
should_generate_seed = Confirm.ask(
|
|
209
|
+
"[bold cyan]Proceed to generate Seed specification?[/]",
|
|
210
|
+
default=True,
|
|
176
211
|
)
|
|
177
|
-
return
|
|
178
212
|
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
if not should_generate_seed:
|
|
214
|
+
console.print(
|
|
215
|
+
"[muted]You can resume later with:[/] "
|
|
216
|
+
f"[bold]ouroboros init start --resume {state.interview_id}[/]"
|
|
217
|
+
)
|
|
218
|
+
return
|
|
181
219
|
|
|
182
|
-
|
|
183
|
-
|
|
220
|
+
# Generate Seed
|
|
221
|
+
seed_path, result = await _generate_seed_from_interview(state, llm_adapter)
|
|
222
|
+
|
|
223
|
+
if result == SeedGenerationResult.CONTINUE_INTERVIEW:
|
|
224
|
+
# Re-open interview for more questions
|
|
225
|
+
console.print()
|
|
226
|
+
print_info("Continuing interview to reduce ambiguity...")
|
|
227
|
+
state.status = InterviewStatus.IN_PROGRESS
|
|
228
|
+
await engine.save_state(state) # Save status change immediately
|
|
229
|
+
|
|
230
|
+
# Continue interview loop (reusing the same helper)
|
|
231
|
+
state = await _run_interview_loop(engine, state)
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
if result == SeedGenerationResult.CANCELLED:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Success - proceed to workflow
|
|
238
|
+
break
|
|
184
239
|
|
|
185
240
|
# Ask if user wants to start workflow
|
|
186
241
|
console.print()
|
|
@@ -196,7 +251,7 @@ async def _run_interview(
|
|
|
196
251
|
async def _generate_seed_from_interview(
|
|
197
252
|
state: InterviewState,
|
|
198
253
|
llm_adapter: LLMAdapter,
|
|
199
|
-
) -> Path | None:
|
|
254
|
+
) -> tuple[Path | None, SeedGenerationResult]:
|
|
200
255
|
"""Generate Seed from completed interview.
|
|
201
256
|
|
|
202
257
|
Args:
|
|
@@ -204,7 +259,7 @@ async def _generate_seed_from_interview(
|
|
|
204
259
|
llm_adapter: LLM adapter for scoring and generation.
|
|
205
260
|
|
|
206
261
|
Returns:
|
|
207
|
-
|
|
262
|
+
Tuple of (path to generated seed file or None, result status).
|
|
208
263
|
"""
|
|
209
264
|
console.print()
|
|
210
265
|
console.print("[bold cyan]Generating Seed specification...[/]")
|
|
@@ -216,7 +271,7 @@ async def _generate_seed_from_interview(
|
|
|
216
271
|
|
|
217
272
|
if score_result.is_err:
|
|
218
273
|
print_error(f"Failed to calculate ambiguity: {score_result.error.message}")
|
|
219
|
-
return None
|
|
274
|
+
return None, SeedGenerationResult.CANCELLED
|
|
220
275
|
|
|
221
276
|
ambiguity_score = score_result.value
|
|
222
277
|
console.print(f"[muted]Ambiguity score: {ambiguity_score.overall_score:.2f}[/]")
|
|
@@ -226,12 +281,24 @@ async def _generate_seed_from_interview(
|
|
|
226
281
|
f"Ambiguity score ({ambiguity_score.overall_score:.2f}) is too high. "
|
|
227
282
|
"Consider more interview rounds to clarify requirements."
|
|
228
283
|
)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
284
|
+
console.print()
|
|
285
|
+
console.print("[bold]What would you like to do?[/]")
|
|
286
|
+
console.print(" [cyan]1[/] - Continue interview with more questions")
|
|
287
|
+
console.print(" [cyan]2[/] - Generate Seed anyway (force)")
|
|
288
|
+
console.print(" [cyan]3[/] - Cancel")
|
|
289
|
+
console.print()
|
|
290
|
+
|
|
291
|
+
choice = Prompt.ask(
|
|
292
|
+
"[yellow]Select option[/]",
|
|
293
|
+
choices=["1", "2", "3"],
|
|
294
|
+
default="1",
|
|
232
295
|
)
|
|
233
|
-
|
|
234
|
-
|
|
296
|
+
|
|
297
|
+
if choice == "1":
|
|
298
|
+
return None, SeedGenerationResult.CONTINUE_INTERVIEW
|
|
299
|
+
elif choice == "3":
|
|
300
|
+
return None, SeedGenerationResult.CANCELLED
|
|
301
|
+
# choice == "2" falls through to generate anyway
|
|
235
302
|
|
|
236
303
|
# Step 2: Generate Seed
|
|
237
304
|
with console.status("[cyan]Generating Seed from interview...[/]", spinner="dots"):
|
|
@@ -240,18 +307,20 @@ async def _generate_seed_from_interview(
|
|
|
240
307
|
if ambiguity_score.is_ready_for_seed:
|
|
241
308
|
seed_result = await generator.generate(state, ambiguity_score)
|
|
242
309
|
else:
|
|
243
|
-
#
|
|
310
|
+
# TODO: Add force=True parameter to SeedGenerator.generate() instead of this hack
|
|
311
|
+
# Creating a modified score to bypass threshold check
|
|
244
312
|
from ouroboros.bigbang.ambiguity import AmbiguityScore as AmbScore
|
|
245
313
|
|
|
314
|
+
FORCED_SCORE_VALUE = 0.19 # Just under threshold (0.2)
|
|
246
315
|
forced_score = AmbScore(
|
|
247
|
-
overall_score=
|
|
316
|
+
overall_score=FORCED_SCORE_VALUE,
|
|
248
317
|
breakdown=ambiguity_score.breakdown,
|
|
249
318
|
)
|
|
250
319
|
seed_result = await generator.generate(state, forced_score)
|
|
251
320
|
|
|
252
321
|
if seed_result.is_err:
|
|
253
322
|
print_error(f"Failed to generate Seed: {seed_result.error.message}")
|
|
254
|
-
return None
|
|
323
|
+
return None, SeedGenerationResult.CANCELLED
|
|
255
324
|
|
|
256
325
|
seed = seed_result.value
|
|
257
326
|
|
|
@@ -261,10 +330,10 @@ async def _generate_seed_from_interview(
|
|
|
261
330
|
|
|
262
331
|
if save_result.is_err:
|
|
263
332
|
print_error(f"Failed to save Seed: {save_result.error.message}")
|
|
264
|
-
return None
|
|
333
|
+
return None, SeedGenerationResult.CANCELLED
|
|
265
334
|
|
|
266
335
|
print_success(f"Seed generated: {seed_path}")
|
|
267
|
-
return seed_path
|
|
336
|
+
return seed_path, SeedGenerationResult.SUCCESS
|
|
268
337
|
|
|
269
338
|
|
|
270
339
|
async def _start_workflow(seed_path: Path, use_orchestrator: bool = False) -> None:
|
|
@@ -344,21 +413,17 @@ def start(
|
|
|
344
413
|
"""
|
|
345
414
|
# Get initial context if not provided
|
|
346
415
|
if not resume and not context:
|
|
347
|
-
console.print(
|
|
348
|
-
"[bold cyan]Welcome to Ouroboros Interview![/]",
|
|
349
|
-
)
|
|
416
|
+
console.print("[bold cyan]Welcome to Ouroboros Interview![/]")
|
|
350
417
|
console.print()
|
|
351
418
|
console.print(
|
|
352
419
|
"This interactive process will help refine your ideas into clear requirements.",
|
|
353
420
|
)
|
|
354
421
|
console.print(
|
|
355
|
-
|
|
422
|
+
"You control when to stop - no arbitrary round limit.",
|
|
356
423
|
)
|
|
357
424
|
console.print()
|
|
358
425
|
|
|
359
|
-
context = Prompt.ask(
|
|
360
|
-
"[bold]What would you like to build?[/]",
|
|
361
|
-
)
|
|
426
|
+
context = Prompt.ask("[bold]What would you like to build?[/]")
|
|
362
427
|
|
|
363
428
|
if not resume and not context:
|
|
364
429
|
print_error("Initial context is required when not resuming.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ouroboros-ai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Self-Improving AI Workflow System
|
|
5
5
|
Author-email: Q00 <jqyu.lee@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -41,18 +41,26 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
<em>The serpent that devours itself to be reborn anew.</em>
|
|
42
42
|
</p>
|
|
43
43
|
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="https://pypi.org/project/ouroboros-ai/"><img src="https://img.shields.io/pypi/v/ouroboros-ai?color=blue" alt="PyPI"></a>
|
|
46
|
+
<a href="https://python.org"><img src="https://img.shields.io/badge/python-3.14+-blue" alt="Python"></a>
|
|
47
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
|
|
48
|
+
</p>
|
|
49
|
+
|
|
44
50
|
<p align="center">
|
|
45
51
|
<a href="#-philosophy">Philosophy</a> •
|
|
46
52
|
<a href="#-the-six-phases">Phases</a> •
|
|
47
53
|
<a href="#-architecture">Architecture</a> •
|
|
54
|
+
<a href="#-prerequisites">Prerequisites</a> •
|
|
48
55
|
<a href="#-quick-start">Start</a> •
|
|
49
|
-
<a href="#-
|
|
56
|
+
<a href="#-roadmap">Roadmap</a> •
|
|
57
|
+
<a href="#-contributing">Contributing</a>
|
|
50
58
|
</p>
|
|
51
59
|
|
|
52
60
|
<br/>
|
|
53
61
|
|
|
54
62
|
<p align="center">
|
|
55
|
-
<code>
|
|
63
|
+
<code>75 modules</code> · <code>1,341 tests</code> · <code>97%+ coverage</code> · <code>v0.2.x</code>
|
|
56
64
|
</p>
|
|
57
65
|
|
|
58
66
|
<br/>
|
|
@@ -352,7 +360,7 @@ src/ouroboros/
|
|
|
352
360
|
|
|
353
361
|
<br/>
|
|
354
362
|
|
|
355
|
-
|
|
363
|
+
**How it works**: Stagnation Detection (4 patterns) → Persona Rotation → Retry with fresh perspective
|
|
356
364
|
|
|
357
365
|
<br/>
|
|
358
366
|
|
|
@@ -415,6 +423,22 @@ not to restart, but to **re-crystallize** with new understanding.
|
|
|
415
423
|
|
|
416
424
|
<br/>
|
|
417
425
|
|
|
426
|
+
## ◈ Prerequisites
|
|
427
|
+
|
|
428
|
+
<br/>
|
|
429
|
+
|
|
430
|
+
| Requirement | Description |
|
|
431
|
+
|-------------|-------------|
|
|
432
|
+
| **Python 3.14+** | Required (uses latest language features) |
|
|
433
|
+
| **Claude Code Max Plan** | For orchestrator mode (no API key needed) |
|
|
434
|
+
| **OR API Key** | OpenRouter, Anthropic, or OpenAI for LiteLLM mode |
|
|
435
|
+
|
|
436
|
+
<br/>
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
<br/>
|
|
441
|
+
|
|
418
442
|
## ◈ Installation
|
|
419
443
|
|
|
420
444
|
<br/>
|
|
@@ -476,7 +500,7 @@ uv run ouroboros init start "I want to build a task management CLI"
|
|
|
476
500
|
uv run ouroboros status health
|
|
477
501
|
```
|
|
478
502
|
|
|
479
|
-
> 📖 **[Full Guide: Running with Claude Code](docs/running-with-claude-code.md)**
|
|
503
|
+
> 📖 **[Full Guide: Running with Claude Code](docs/running-with-claude-code.md)** | **[CLI Reference](docs/cli-reference.md)**
|
|
480
504
|
|
|
481
505
|
<br/>
|
|
482
506
|
|
|
@@ -563,22 +587,49 @@ uv run ruff format src/
|
|
|
563
587
|
|
|
564
588
|
<br/>
|
|
565
589
|
|
|
590
|
+
## ◈ Contributing
|
|
591
|
+
|
|
592
|
+
<br/>
|
|
593
|
+
|
|
594
|
+
Contributions are welcome! Please see:
|
|
595
|
+
|
|
596
|
+
- **Issues**: [GitHub Issues](https://github.com/Q00/ouroboros/issues) for bugs and feature requests
|
|
597
|
+
- **Discussions**: [GitHub Discussions](https://github.com/Q00/ouroboros/discussions) for questions and ideas
|
|
598
|
+
|
|
599
|
+
<br/>
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
<br/>
|
|
604
|
+
|
|
566
605
|
## ◈ Roadmap
|
|
567
606
|
|
|
568
607
|
<br/>
|
|
569
608
|
|
|
609
|
+
### Completed
|
|
610
|
+
|
|
570
611
|
```
|
|
571
|
-
[■■■■■■■■■■] Epic 0 Foundation
|
|
572
|
-
[■■■■■■■■■■] Epic 1 Big Bang
|
|
573
|
-
[■■■■■■■■■■] Epic 2 PAL Router
|
|
574
|
-
[■■■■■■■■■■] Epic 3 Double Diamond
|
|
575
|
-
[■■■■■■■■■■] Epic 4 Resilience
|
|
576
|
-
[■■■■■■■■■■] Epic 5 Evaluation
|
|
577
|
-
[■■■■■■■■■■] Epic 6 Drift Control
|
|
578
|
-
[■■■■■■■■■■] Epic 7 Secondary Loop
|
|
579
|
-
[■■■■■■■■■■] Epic 8 Orchestrator
|
|
612
|
+
[■■■■■■■■■■] Epic 0 Foundation ✓
|
|
613
|
+
[■■■■■■■■■■] Epic 1 Big Bang ✓
|
|
614
|
+
[■■■■■■■■■■] Epic 2 PAL Router ✓
|
|
615
|
+
[■■■■■■■■■■] Epic 3 Double Diamond ✓
|
|
616
|
+
[■■■■■■■■■■] Epic 4 Resilience ✓
|
|
617
|
+
[■■■■■■■■■■] Epic 5 Evaluation ✓
|
|
618
|
+
[■■■■■■■■■■] Epic 6 Drift Control ✓
|
|
619
|
+
[■■■■■■■■■■] Epic 7 Secondary Loop ✓
|
|
620
|
+
[■■■■■■■■■■] Epic 8 Orchestrator ✓
|
|
580
621
|
```
|
|
581
622
|
|
|
623
|
+
### Upcoming
|
|
624
|
+
|
|
625
|
+
| Feature | Description | Status |
|
|
626
|
+
|---------|-------------|--------|
|
|
627
|
+
| **Worker MCP** | MCP server for distributed task execution | Planned |
|
|
628
|
+
| **TUI Enhancement** | Rich terminal UI with real-time progress | Planned |
|
|
629
|
+
| **AC Tree Visualization** | Interactive acceptance criteria graph | Planned |
|
|
630
|
+
| **Plugin System** | Custom evaluators and personas | Planned |
|
|
631
|
+
| **Web Dashboard** | Execution monitoring and analytics | Planned |
|
|
632
|
+
|
|
582
633
|
<br/>
|
|
583
634
|
|
|
584
635
|
---
|
|
@@ -2,14 +2,14 @@ ouroboros/__init__.py,sha256=lmQgHmNOWxGlmwayNvp1ckCuJycL8WzX5Y-7IzrFaVM,701
|
|
|
2
2
|
ouroboros/__main__.py,sha256=f_qnL0zPJwh9kfQqynX5adpqzj8ilj94zW5Q2loqGxE,168
|
|
3
3
|
ouroboros/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
ouroboros/bigbang/__init__.py,sha256=9xGqOYwMKBifb7QVwonc_wndNLMZb7ZH7xgMHaz_70A,951
|
|
5
|
-
ouroboros/bigbang/ambiguity.py,sha256=
|
|
6
|
-
ouroboros/bigbang/interview.py,sha256=
|
|
5
|
+
ouroboros/bigbang/ambiguity.py,sha256=5KM8xjATknjLZguVa90Yii6o3pzXE4PU4BJIP6Ii938,17955
|
|
6
|
+
ouroboros/bigbang/interview.py,sha256=ku1MVppSmIS9OZeiqC208el4ZqoYYQwT78ycLenwID4,17200
|
|
7
7
|
ouroboros/bigbang/seed_generator.py,sha256=7MY9a7Eua_zVGDWIVDlzOZJjeAwz0DRatXJg0PvMgiY,20082
|
|
8
8
|
ouroboros/cli/__init__.py,sha256=CRpxsqJadZL7bCS-yrULWC51tqPKfPsxQLgt0JiwP4g,225
|
|
9
9
|
ouroboros/cli/main.py,sha256=ldvqtVpw2xZwE8G7M34qY_7qg0RuNiydjdmmU-hdJvM,1485
|
|
10
10
|
ouroboros/cli/commands/__init__.py,sha256=LZpEvU80R4Cq0LwgkwOluEGNsmmJ9K7roeDQ6bsbbDc,193
|
|
11
11
|
ouroboros/cli/commands/config.py,sha256=kcqi0Wo09oo1MMyZIX4k2IDICV1SAX6HzAXZaIJGdKY,2100
|
|
12
|
-
ouroboros/cli/commands/init.py,sha256=
|
|
12
|
+
ouroboros/cli/commands/init.py,sha256=y2hlHnqFkc9-44_mJUs3GmQvX4Tg6yTIyb0e0KCPl30,16130
|
|
13
13
|
ouroboros/cli/commands/run.py,sha256=DnxfbSdATDIaNYJXLcwAcR9NqNVGkVlHgYJImaSVn4I,6328
|
|
14
14
|
ouroboros/cli/commands/status.py,sha256=Bnqpj1UkqhpBPYA11DV-Z63Bz8pjrebhlzeMKwz3_Ps,2217
|
|
15
15
|
ouroboros/cli/formatters/__init__.py,sha256=-Ik7KXajaIExBxSAp5iYp8gO9SfXudGjyDe2nm2_msw,691
|
|
@@ -75,8 +75,8 @@ ouroboros/routing/tiers.py,sha256=QhBQUOo2-h5Z3dEtC0lcOzkRnqTi2W7Jl46750AVNig,73
|
|
|
75
75
|
ouroboros/secondary/__init__.py,sha256=kYQ7C4bnBzwDlPrU8qZrOPr2ZuTBaftGktOXl5WZl5Q,1123
|
|
76
76
|
ouroboros/secondary/scheduler.py,sha256=sPVVWJ1q0yewRAM-Rm1j_HMerSe4cavIvP9z4xlUuL4,13737
|
|
77
77
|
ouroboros/secondary/todo_registry.py,sha256=4W3C9Uro29VrVLCPKUlpH_BYpzQSbRNW1oMnDYyEhEw,13880
|
|
78
|
-
ouroboros_ai-0.
|
|
79
|
-
ouroboros_ai-0.
|
|
80
|
-
ouroboros_ai-0.
|
|
81
|
-
ouroboros_ai-0.
|
|
82
|
-
ouroboros_ai-0.
|
|
78
|
+
ouroboros_ai-0.3.0.dist-info/METADATA,sha256=J5my7Xvr9PRNCn081N7t6ER3cvRP7AFwWilWjcf5fUo,21321
|
|
79
|
+
ouroboros_ai-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
80
|
+
ouroboros_ai-0.3.0.dist-info/entry_points.txt,sha256=MoETHup6rVkR6AsyjoRzAgIuvVtYYm3Jw40itV3_VyI,53
|
|
81
|
+
ouroboros_ai-0.3.0.dist-info/licenses/LICENSE,sha256=n2X-q26TqpXnoBo0t_WouhFxWw663_q5FmbYDZayoHo,1060
|
|
82
|
+
ouroboros_ai-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|