Skip to content

Commit 89829a0

Browse files
committed
Version v0.9.9 done
1 parent c1c87fd commit 89829a0

24 files changed

Lines changed: 1152 additions & 33 deletions

aideator/search/providers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@
1212
from typing import Any
1313

1414

15+
class SignalType(str, Enum):
16+
"""Categories for signal evidence quality and type."""
17+
18+
PAIN_COMPLAINT = "pain_complaint"
19+
FEATURE_REQUEST = "feature_request"
20+
COMPETITOR_WEAKNESS = "competitor_weakness"
21+
MARKET_DATA = "market_data"
22+
ANECDOTAL_POSITIVE = "anecdotal_positive"
23+
SEO_FILLER = "seo_filler"
24+
UNSPECIFIED = "unspecified"
25+
26+
1527
class ProviderStatus(Enum):
1628
"""Health check status for search providers."""
1729

@@ -33,13 +45,17 @@ class SearchResult:
3345
snippet: Text excerpt / summary
3446
source: Provider that returned this result (e.g. 'tavily', 'builtin')
3547
score: Relevance score (0.0 to 1.0, provider-dependent)
48+
signal_type: Categorized type of signal (e.g. 'pain_complaint')
49+
confidence: Confidence score in the classification (0.0 to 1.0)
3650
"""
3751

3852
title: str
3953
url: str
4054
snippet: str
4155
source: str = ""
4256
score: float = 0.0
57+
signal_type: SignalType = SignalType.UNSPECIFIED
58+
confidence: float = 0.0
4359

4460

4561
@dataclass(frozen=True)

api/web.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ def _idea_rows(user_id: UUID | None = None) -> list[dict[str, object]]:
137137
"created_at": _fmt_ts(idea.created_at),
138138
"runs_count": len(idea_runs),
139139
"last_run_status": last_run.status if last_run else "pending",
140+
"validation_status": (
141+
idea.status.value if hasattr(idea.status, "value")
142+
else str(idea.status)
143+
),
140144
}
141145
)
142146
rows.sort(key=lambda item: str(item["created_at"]), reverse=True)
@@ -489,6 +493,10 @@ def idea_detail_page(request: Request, idea_id: UUID) -> HTMLResponse:
489493
"target_user": idea.target_user,
490494
"context": idea.context,
491495
"created_at": _fmt_ts(idea.created_at),
496+
"status": (
497+
idea.status.value if hasattr(idea.status, "value")
498+
else str(idea.status)
499+
),
492500
},
493501
"runs": _run_rows(idea_filter=idea_id),
494502
"available_modes": ["local-only", "hybrid", "cloud-enabled"],

db/ideas.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from db.base import db_session, initialize_db
99
from db.schema import IdeaModel, WorkspaceMemberModel
10-
from models.idea import Idea
10+
from models.idea import Idea, ValidationStatus
1111

1212
LOGGER = logging.getLogger("db.ideas")
1313

@@ -27,6 +27,7 @@ def _to_model(idea: Idea) -> IdeaModel:
2727
created_at=idea.created_at,
2828
tier=idea.tier,
2929
brand_hex=idea.brand_hex,
30+
validation_status=idea.status.value if hasattr(idea.status, "value") else str(idea.status),
3031
workspace_id=str(idea.workspace_id) if idea.workspace_id else None,
3132
)
3233

@@ -39,6 +40,7 @@ def _from_model(model: IdeaModel) -> Idea:
3940
context=model.context,
4041
tier=model.tier or "Bronze",
4142
brand_hex=model.brand_hex or "#888888",
43+
status=ValidationStatus(model.validation_status or "desk_research"),
4244
workspace_id=UUID(model.workspace_id) if model.workspace_id else None,
4345
)
4446
idea.idea_id = UUID(model.idea_id)
@@ -57,6 +59,10 @@ def save_idea(idea: Idea, user_id: UUID | None = None) -> Idea:
5759
model.context = idea.context
5860
model.tier = idea.tier
5961
model.brand_hex = idea.brand_hex
62+
model.validation_status = (
63+
idea.status.value if hasattr(idea.status, "value")
64+
else str(idea.status)
65+
)
6066
if idea.workspace_id:
6167
model.workspace_id = str(idea.workspace_id)
6268
if user_id:

db/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class IdeaModel(Base):
2727
)
2828
tier: Mapped[str | None] = mapped_column(String(50), default="Bronze")
2929
brand_hex: Mapped[str | None] = mapped_column(String(7), default="#888888")
30+
validation_status: Mapped[str] = mapped_column(String(50), default="desk_research")
3031
user_id: Mapped[str | None] = mapped_column(
3132
String(36), ForeignKey("users.user_id"), nullable=True
3233
)

docs/idea-11111111-1111-1111-1111-111111111111.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Idea Report 11111111-1111-1111-1111-111111111111
22

3+
## 🏆 Current Validation Phase
4+
**Stage:** `Desk Research`
5+
36
<div class="reveal">
47
## Market Demand — <span class="score-display animate-score" data-target="62">0</span>/100
58
**Executive Summary:** Early demand is plausible but needs focused validation.

engine/analyst.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import logging
5+
from typing import Any
56

67
from aideator.llm.registry import get_provider
78
from api.config import settings
@@ -13,7 +14,7 @@ async def analyze_dimensions(
1314
*,
1415
title: str,
1516
description: str,
16-
citations: list[dict[str, str]],
17+
citations: list[dict[str, Any]],
1718
) -> dict[str, object]:
1819
"""Analyze collected signals across dimensions.
1920
@@ -32,7 +33,13 @@ async def analyze_dimensions(
3233
provider = get_provider(settings)
3334

3435
signals_text = "\n".join(
35-
[f"- [{c['source_id']}] {c['content']} (URL: {c['url']})" for c in citations]
36+
[
37+
f"- [{c['source_id']}] "
38+
f"[Type: {c.get('type', 'unknown')}, "
39+
f"Confidence: {c.get('confidence', 0.5)}] "
40+
f"{c['content']} (URL: {c['url']})"
41+
for c in citations
42+
]
3643
)
3744

3845
prompt = f"""You are a High-Precision Market Analyst.
@@ -51,6 +58,13 @@ async def analyze_dimensions(
5158
4. MARKET_SIZING: Based on signals, estimate the TAM (Total Addressable Market),
5259
SAM (Serviceable Addressable Market), and SOM (Serviceable Obtainable Market) in USD.
5360
61+
WEIGHTING RULES:
62+
- PRIORITIZE signals with type 'pain_complaint', 'feature_request', or 'competitor_weakness'.
63+
These are high-intent signals.
64+
- TREAT signals with type 'seo_filler' or low confidence (< 0.4) as noise.
65+
Do not let them drive major score changes.
66+
- HIGH CONFIDENCE 'pain_complaint' is your strongest evidence for Demand.
67+
5468
For each of the first three dimensions, provide:
5569
- "strengths": Signals clearly supporting the case.
5670
- "weaknesses": Signals suggesting risks or lack of market fit.

engine/battle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
class BattleOrchestrator:
1616
"""Orchestrates Bull and Bear agents for adversarial analysis."""
1717

18-
def __init__(self, title: str, description: str, signals: list[dict[str, str]]):
18+
def __init__(self, title: str, description: str, signals: list[dict[str, Any]]):
1919
self.title = title
2020
self.description = description
2121
self.signals = signals

engine/experiments.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Experiment Engine for generating behavioral validation kits (Smoke Tests)."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import logging
7+
from typing import Any
8+
9+
from aideator.llm.registry import get_provider
10+
from models.report import ExperimentKit
11+
12+
LOGGER = logging.getLogger("engine.experiments")
13+
14+
15+
async def generate_experiment_kit(
16+
*,
17+
title: str,
18+
description: str,
19+
analysis_results: dict[str, Any] | None = None,
20+
) -> ExperimentKit:
21+
"""Generate a behavioral experiment blueprint based on the idea and analysis.
22+
23+
Args:
24+
title: Idea title.
25+
description: Idea description.
26+
analysis_results: Results from Dimensional Analysis for context.
27+
28+
Returns:
29+
ExperimentKit object with copy, metrics, and tools.
30+
"""
31+
try:
32+
from api.config import settings
33+
provider = get_provider(settings)
34+
35+
# Context from analysis if available
36+
if analysis_results and "details" in analysis_results:
37+
context_text = analysis_results["details"][:200]
38+
else:
39+
context_text = "the core problem"
40+
41+
prompt = f"""You are an Expert Growth Marketer and Experiment Designer.
42+
Your task is to design a "Smoke Test" experiment for a new business idea.
43+
44+
IDEA:
45+
Title: {title}
46+
Description: {description}
47+
Context: {context_text}
48+
49+
The experiment should be designed to move from "intent" (interviews)
50+
to "commitment" (behavioral data).
51+
52+
GENERATION RULES:
53+
1. HYPOTHESIS: State it as "If I [action], then [number/percent] of
54+
[audience] will [commitment action]."
55+
2. LANDING PAGE COPY: Use the PAS (Problem, Agitation, Solution) framework.
56+
- Headline: Attention-grabbing benefit.
57+
- Hero Subheadline: Clear value prop.
58+
- Pain Points: 3 bullet points that hurt.
59+
- Solution: How we fix it.
60+
- CTA: Strong action (e.g., "Join the Early Access", "Pre-order Now").
61+
3. SUCCESS METRIC: Define a "Fail-Fast" threshold for a 48-hour or 1-week test.
62+
4. TOOL STACK: Recommend 2-3 specific no-code tools (e.g., Carrd, Framer, Tally, Loops, Stripe).
63+
64+
Return a JSON object with:
65+
{{
66+
"hypothesis": "...",
67+
"method": "Smoke Test / Landing Page",
68+
"landing_page_copy": {{
69+
"headline": "...",
70+
"subheadline": "...",
71+
"pain_points": ["...", "...", "..."],
72+
"solution": "...",
73+
"cta": "..."
74+
}},
75+
"success_metric": "...",
76+
"tool_stack": ["...", "..."]
77+
}}
78+
"""
79+
messages = [{"role": "user", "content": prompt}]
80+
response = await provider.generate(messages, temperature=0.3)
81+
82+
content = response.content.strip()
83+
if "```json" in content:
84+
content = content.split("```json")[1].split("```")[0].strip()
85+
elif "```" in content:
86+
content = content.split("```")[1].split("```")[0].strip()
87+
88+
data = json.loads(content)
89+
90+
# Ensure landing_page_copy is flattened correctly if needed,
91+
# but our model expects a dict[str, str].
92+
# Let's adjust the data to match dict[str, str] by joining pain points.
93+
copy = data.get("landing_page_copy", {})
94+
if isinstance(copy.get("pain_points"), list):
95+
copy["pain_points"] = "\n".join(copy["pain_points"])
96+
97+
return ExperimentKit(
98+
hypothesis=data.get("hypothesis", "If I launch a landing page, people will sign up."),
99+
method=data.get("method", "Smoke Test"),
100+
landing_page_copy=copy,
101+
success_metric=data.get("success_metric", "5% conversion rate"),
102+
tool_stack=data.get("tool_stack", ["Carrd", "Tally"])
103+
)
104+
105+
except Exception as e:
106+
LOGGER.warning(f"Experiment generation failed: {e}")
107+
return ExperimentKit(
108+
hypothesis=f"If I build a landing page for {title}, people will express interest.",
109+
method="Smoke Test",
110+
landing_page_copy={
111+
"headline": f"Stop struggling with {title}",
112+
"subheadline": description[:100],
113+
"cta": "Get Early Access"
114+
},
115+
success_metric="10 signups from 200 visits",
116+
tool_stack=["Carrd", "Typeform"]
117+
)

engine/interviewer.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Engine for generating actionable user interview toolkits."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import logging
7+
from typing import Any
8+
9+
from aideator.llm.registry import get_provider
10+
from api.config import settings
11+
from models.report import InterviewKit
12+
13+
LOGGER = logging.getLogger("engine.interviewer")
14+
15+
async def generate_interview_kit(
16+
*,
17+
title: str,
18+
description: str,
19+
analysis: dict[str, Any],
20+
signals: list[dict[str, Any]]
21+
) -> InterviewKit:
22+
"""Generate a customized interview kit based on validation results.
23+
24+
Args:
25+
title: Idea title
26+
description: Idea description
27+
analysis: Dimensional analysis results from Node 2
28+
signals: Classified citations with weights
29+
30+
Returns:
31+
InterviewKit object
32+
"""
33+
try:
34+
provider = get_provider(settings)
35+
36+
# Identify the weakest dimension to focus the interview on
37+
# Analysis structure: {"demand": {"strengths": [], "weaknesses": [], ...}, ...}
38+
dimensions = ["demand", "competition", "viability"]
39+
weakness_counts = {d: len(analysis.get(d, {}).get("weaknesses", [])) for d in dimensions}
40+
# Sort by most weaknesses
41+
primary_focus = max(weakness_counts, key=lambda k: weakness_counts[k])
42+
43+
signals_summary = "\n".join([
44+
f"- [{s.get('source_id')}] Type: {s.get('type')}, "
45+
f"Content: {(s.get('content') or '')[:100]}..."
46+
for s in signals[:5]
47+
])
48+
49+
prompt = f"""You are a Master User Researcher specialized in "The Mom Test" methodology.
50+
Your goal is to create a User Interview Toolkit for the business idea: "{title}".
51+
52+
CONTEXT:
53+
{description}
54+
55+
CURRENT FINDINGS:
56+
- Primary Risk Area: {primary_focus.upper()}
57+
- Signals Found:
58+
{signals_summary}
59+
60+
TASK:
61+
Generate an interview toolkit that helps the user validate their riskiest assumptions
62+
through real conversations.
63+
64+
REQUIREMENTS:
65+
1. SCRIPT: 5-7 non-leading questions. Focus on the user's PAST behavior, not their future opinions.
66+
Include a "Rationale" for each question (e.g. "To test if they currently spend money on X").
67+
2. OUTREACH: 3 templates (LinkedIn, Email, Reddit/Discord) that are short, personal, and NOT salesy.
68+
3. TRACKER: 5-8 column headers for a spreadsheet to track responses.
69+
(e.g. "Current Solution", "Monthly Pain Score (1-10)", "Budget Owner?").
70+
71+
Return ONLY a JSON object with this structure:
72+
{{
73+
"script": [
74+
{{ "question": "...", "rationale": "..." }}
75+
],
76+
"outreach_templates": {{
77+
"linkedin": "...",
78+
"email": "...",
79+
"reddit": "..."
80+
}},
81+
"response_tracker": ["Column 1", "Column 2", ...]
82+
}}
83+
"""
84+
messages = [{"role": "user", "content": prompt}]
85+
response = await provider.generate(messages, temperature=0.5)
86+
87+
content = response.content.strip()
88+
if "```json" in content:
89+
content = content.split("```json")[1].split("```")[0].strip()
90+
elif "```" in content:
91+
content = content.split("```")[1].split("```")[0].strip()
92+
93+
data = json.loads(content)
94+
95+
return InterviewKit(
96+
script=data.get("script", []),
97+
outreach_templates=data.get("outreach_templates", {}),
98+
response_tracker=data.get("response_tracker", [])
99+
)
100+
101+
except Exception as e:
102+
LOGGER.error(f"Interview kit generation failed: {e}", exc_info=True)
103+
# Fallback to a generic kit
104+
return InterviewKit(
105+
script=[{
106+
"question": "Can you tell me about the last time you tried to solve [Problem]?",
107+
"rationale": "Discovery"
108+
}],
109+
outreach_templates={
110+
"email": "Hi, I'm researching [Problem] and would love to hear your story."
111+
},
112+
response_tracker=["Name", "Role", "Last Time Problem Occurred", "Current Workaround"]
113+
)

0 commit comments

Comments
 (0)