crowdmind 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crowdmind/__init__.py +22 -0
- crowdmind/cli.py +371 -0
- crowdmind/config.py +177 -0
- crowdmind/ideate/__init__.py +13 -0
- crowdmind/ideate/features.py +327 -0
- crowdmind/market/__init__.py +19 -0
- crowdmind/market/analysis.py +552 -0
- crowdmind/persona_packs/consumer.yaml +83 -0
- crowdmind/persona_packs/devtools.yaml +83 -0
- crowdmind/persona_packs/enterprise.yaml +83 -0
- crowdmind/persona_packs/saas.yaml +83 -0
- crowdmind/report/__init__.py +12 -0
- crowdmind/report/markdown.py +340 -0
- crowdmind/research/__init__.py +20 -0
- crowdmind/research/codebase.py +362 -0
- crowdmind/research/github.py +240 -0
- crowdmind/research/hackernews.py +163 -0
- crowdmind/research/multi.py +387 -0
- crowdmind/research/reddit.py +335 -0
- crowdmind/validate/__init__.py +24 -0
- crowdmind/validate/panel.py +343 -0
- crowdmind/validate/personas.py +342 -0
- crowdmind-0.1.0.dist-info/METADATA +364 -0
- crowdmind-0.1.0.dist-info/RECORD +27 -0
- crowdmind-0.1.0.dist-info/WHEEL +4 -0
- crowdmind-0.1.0.dist-info/entry_points.txt +2 -0
- crowdmind-0.1.0.dist-info/licenses/LICENSE +21 -0
crowdmind/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrowdMind - AI-Powered Research & Validation for Products
|
|
3
|
+
|
|
4
|
+
Validate your product ideas with simulated user panels,
|
|
5
|
+
research market pain points, and generate feature ideas.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from crowdmind.validate.panel import run_evaluation as analyze
|
|
11
|
+
from crowdmind.validate.panel import run_evaluation as validate
|
|
12
|
+
from crowdmind.research.multi import run_multi_research as research
|
|
13
|
+
from crowdmind.validate.personas import Persona, PersonaPack
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"analyze",
|
|
17
|
+
"validate",
|
|
18
|
+
"research",
|
|
19
|
+
"Persona",
|
|
20
|
+
"PersonaPack",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
crowdmind/cli.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrowdMind CLI
|
|
3
|
+
|
|
4
|
+
A command-line interface for product research and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="crowdmind",
|
|
17
|
+
help="AI-Powered Research & Validation for Products",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Common options
|
|
25
|
+
def common_options(func):
|
|
26
|
+
"""Decorator for common CLI options."""
|
|
27
|
+
func = typer.Option(None, "--personas", "-p", help="Number of personas to use")(func)
|
|
28
|
+
return func
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def analyze(
|
|
33
|
+
path: Path = typer.Argument(..., help="Path to project or README file"),
|
|
34
|
+
personas: int = typer.Option(10, "--personas", "-p", help="Number of personas"),
|
|
35
|
+
pack: Optional[str] = typer.Option(None, "--pack", help="Persona pack (developers, enterprise, mixed)"),
|
|
36
|
+
users: Optional[List[str]] = typer.Option(None, "--users", "-u", help="Custom user types"),
|
|
37
|
+
provider: str = typer.Option("anthropic", "--provider", help="LLM provider"),
|
|
38
|
+
model: Optional[str] = typer.Option(None, "--model", "-m", help="Model to use"),
|
|
39
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
|
|
40
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
41
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Run full analysis pipeline on a project.
|
|
45
|
+
|
|
46
|
+
Includes: codebase analysis, research, ideation, and validation.
|
|
47
|
+
"""
|
|
48
|
+
from crowdmind.research.codebase import run_analysis as analyze_codebase
|
|
49
|
+
from crowdmind.research.multi import run_multi_research
|
|
50
|
+
from crowdmind.ideate.features import run_ideation
|
|
51
|
+
from crowdmind.validate.panel import run_evaluation
|
|
52
|
+
from crowdmind.report.markdown import generate_report
|
|
53
|
+
|
|
54
|
+
if not quiet:
|
|
55
|
+
console.print(Panel.fit(
|
|
56
|
+
"[bold blue]CrowdMind Analysis[/bold blue]\n"
|
|
57
|
+
f"Project: {path}",
|
|
58
|
+
border_style="blue"
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
results = {}
|
|
62
|
+
|
|
63
|
+
with Progress(
|
|
64
|
+
SpinnerColumn(),
|
|
65
|
+
TextColumn("[progress.description]{task.description}"),
|
|
66
|
+
console=console,
|
|
67
|
+
disable=quiet,
|
|
68
|
+
) as progress:
|
|
69
|
+
# Step 1: Codebase analysis
|
|
70
|
+
task = progress.add_task("Analyzing codebase...", total=None)
|
|
71
|
+
results["codebase"] = analyze_codebase(path, verbose=verbose)
|
|
72
|
+
progress.update(task, completed=True)
|
|
73
|
+
|
|
74
|
+
# Step 2: Research
|
|
75
|
+
task = progress.add_task("Researching market...", total=None)
|
|
76
|
+
results["research"] = run_multi_research(use_cache=True, verbose=verbose)
|
|
77
|
+
progress.update(task, completed=True)
|
|
78
|
+
|
|
79
|
+
# Step 3: Ideation
|
|
80
|
+
task = progress.add_task("Generating ideas...", total=None)
|
|
81
|
+
results["ideation"] = run_ideation(num_ideas=5, verbose=verbose)
|
|
82
|
+
progress.update(task, completed=True)
|
|
83
|
+
|
|
84
|
+
# Step 4: Validation
|
|
85
|
+
task = progress.add_task("Validating with personas...", total=None)
|
|
86
|
+
readme_path = path / "README.md" if path.is_dir() else path
|
|
87
|
+
if readme_path.exists():
|
|
88
|
+
readme_content = readme_path.read_text()
|
|
89
|
+
results["validation"] = run_evaluation(
|
|
90
|
+
readme_content=readme_content,
|
|
91
|
+
verbose=verbose,
|
|
92
|
+
num_agents=personas,
|
|
93
|
+
)
|
|
94
|
+
progress.update(task, completed=True)
|
|
95
|
+
|
|
96
|
+
# Generate report
|
|
97
|
+
report = generate_report(results)
|
|
98
|
+
|
|
99
|
+
if output:
|
|
100
|
+
output.write_text(report)
|
|
101
|
+
if not quiet:
|
|
102
|
+
console.print(f"\n[green]Report saved to:[/green] {output}")
|
|
103
|
+
else:
|
|
104
|
+
console.print(report)
|
|
105
|
+
|
|
106
|
+
# Summary
|
|
107
|
+
if not quiet and results.get("validation"):
|
|
108
|
+
val = results["validation"]
|
|
109
|
+
console.print(Panel(
|
|
110
|
+
f"[bold]Star Rate:[/bold] {val.get('star_rate', 0)}%\n"
|
|
111
|
+
f"[bold]Avg Score:[/bold] {val.get('avg_star', 0)}/10\n"
|
|
112
|
+
f"[bold]Personas:[/bold] {val.get('agents_evaluated', 0)}",
|
|
113
|
+
title="Validation Summary",
|
|
114
|
+
border_style="green" if val.get('star_rate', 0) >= 50 else "yellow"
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def validate(
|
|
120
|
+
idea: str = typer.Argument(..., help="Idea or README content to validate"),
|
|
121
|
+
personas: int = typer.Option(10, "--personas", "-p", help="Number of personas"),
|
|
122
|
+
pack: Optional[str] = typer.Option(None, "--pack", help="Persona pack"),
|
|
123
|
+
categories: Optional[List[str]] = typer.Option(None, "--categories", "-c", help="Persona categories"),
|
|
124
|
+
provider: str = typer.Option("anthropic", "--provider", help="LLM provider"),
|
|
125
|
+
model: Optional[str] = typer.Option(None, "--model", "-m", help="Model to use"),
|
|
126
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file (JSON)"),
|
|
127
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
128
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
|
|
129
|
+
):
|
|
130
|
+
"""
|
|
131
|
+
Validate a single idea with AI personas.
|
|
132
|
+
|
|
133
|
+
Pass a product description, README content, or path to README file.
|
|
134
|
+
"""
|
|
135
|
+
from crowdmind.validate.panel import run_evaluation
|
|
136
|
+
import json
|
|
137
|
+
|
|
138
|
+
# Check if idea is a file path
|
|
139
|
+
idea_path = Path(idea)
|
|
140
|
+
if idea_path.exists():
|
|
141
|
+
content = idea_path.read_text()
|
|
142
|
+
else:
|
|
143
|
+
content = idea
|
|
144
|
+
|
|
145
|
+
if not quiet:
|
|
146
|
+
console.print(Panel.fit(
|
|
147
|
+
"[bold blue]CrowdMind Validation[/bold blue]\n"
|
|
148
|
+
f"Personas: {personas}",
|
|
149
|
+
border_style="blue"
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
result = run_evaluation(
|
|
153
|
+
readme_content=content,
|
|
154
|
+
verbose=verbose and not quiet,
|
|
155
|
+
num_agents=personas,
|
|
156
|
+
categories=categories,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if output:
|
|
160
|
+
output.write_text(json.dumps(result, indent=2))
|
|
161
|
+
if not quiet:
|
|
162
|
+
console.print(f"\n[green]Results saved to:[/green] {output}")
|
|
163
|
+
|
|
164
|
+
# Display results
|
|
165
|
+
if not quiet:
|
|
166
|
+
table = Table(title="Validation Results")
|
|
167
|
+
table.add_column("Metric", style="cyan")
|
|
168
|
+
table.add_column("Value", style="green")
|
|
169
|
+
|
|
170
|
+
table.add_row("Star Rate", f"{result.get('star_rate', 0)}%")
|
|
171
|
+
table.add_row("Avg Star Likelihood", f"{result.get('avg_star', 0)}/10")
|
|
172
|
+
table.add_row("Total Score", f"{result.get('total_score', 0)}/100")
|
|
173
|
+
table.add_row("Personas Evaluated", str(result.get("agents_evaluated", 0)))
|
|
174
|
+
|
|
175
|
+
console.print(table)
|
|
176
|
+
|
|
177
|
+
# Category breakdown
|
|
178
|
+
if result.get("by_category"):
|
|
179
|
+
console.print("\n[bold]By Category:[/bold]")
|
|
180
|
+
for cat, data in sorted(result["by_category"].items(), key=lambda x: x[1]["avg"], reverse=True):
|
|
181
|
+
console.print(f" {cat}: {data['avg']}/10")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.command()
|
|
185
|
+
def research(
|
|
186
|
+
path: Optional[Path] = typer.Argument(None, help="Path to project (optional)"),
|
|
187
|
+
sources: List[str] = typer.Option(
|
|
188
|
+
["reddit", "hackernews", "github"],
|
|
189
|
+
"--sources", "-s",
|
|
190
|
+
help="Sources to research"
|
|
191
|
+
),
|
|
192
|
+
topics: Optional[List[str]] = typer.Option(None, "--topics", "-t", help="Custom topics to search"),
|
|
193
|
+
no_cache: bool = typer.Option(False, "--no-cache", help="Skip cache"),
|
|
194
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
|
|
195
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
196
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
|
|
197
|
+
):
|
|
198
|
+
"""
|
|
199
|
+
Research market pain points from multiple sources.
|
|
200
|
+
|
|
201
|
+
Sources: reddit, hackernews, github
|
|
202
|
+
"""
|
|
203
|
+
from crowdmind.research.multi import run_multi_research, get_multi_research_summary
|
|
204
|
+
import json
|
|
205
|
+
|
|
206
|
+
if not quiet:
|
|
207
|
+
console.print(Panel.fit(
|
|
208
|
+
"[bold blue]CrowdMind Research[/bold blue]\n"
|
|
209
|
+
f"Sources: {', '.join(sources)}",
|
|
210
|
+
border_style="blue"
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
result = run_multi_research(
|
|
214
|
+
use_cache=not no_cache,
|
|
215
|
+
verbose=verbose and not quiet,
|
|
216
|
+
sources=sources,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if output:
|
|
220
|
+
output.write_text(json.dumps(result, indent=2))
|
|
221
|
+
if not quiet:
|
|
222
|
+
console.print(f"\n[green]Results saved to:[/green] {output}")
|
|
223
|
+
|
|
224
|
+
# Display summary
|
|
225
|
+
if not quiet:
|
|
226
|
+
summary = get_multi_research_summary()
|
|
227
|
+
console.print(summary)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.command()
|
|
231
|
+
def market(
|
|
232
|
+
path: Path = typer.Argument(..., help="Path to project or README"),
|
|
233
|
+
personas: int = typer.Option(10, "--personas", "-p", help="Number of buyer personas"),
|
|
234
|
+
no_cache: bool = typer.Option(False, "--no-cache", help="Skip cache"),
|
|
235
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
|
|
236
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
237
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
|
|
238
|
+
):
|
|
239
|
+
"""
|
|
240
|
+
Run market analysis on a project.
|
|
241
|
+
|
|
242
|
+
Includes: buyer personas, pricing analysis, success predictions.
|
|
243
|
+
"""
|
|
244
|
+
from crowdmind.market.analysis import run_full_market_analysis, get_market_summary
|
|
245
|
+
import json
|
|
246
|
+
|
|
247
|
+
if not quiet:
|
|
248
|
+
console.print(Panel.fit(
|
|
249
|
+
"[bold blue]CrowdMind Market Analysis[/bold blue]\n"
|
|
250
|
+
f"Project: {path}",
|
|
251
|
+
border_style="blue"
|
|
252
|
+
))
|
|
253
|
+
|
|
254
|
+
result = run_full_market_analysis(
|
|
255
|
+
project_path=path,
|
|
256
|
+
use_cache=not no_cache,
|
|
257
|
+
verbose=verbose and not quiet,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if output:
|
|
261
|
+
output.write_text(json.dumps(result, indent=2))
|
|
262
|
+
if not quiet:
|
|
263
|
+
console.print(f"\n[green]Results saved to:[/green] {output}")
|
|
264
|
+
|
|
265
|
+
if not quiet:
|
|
266
|
+
summary = get_market_summary()
|
|
267
|
+
console.print(summary)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.command()
|
|
271
|
+
def personas():
|
|
272
|
+
"""
|
|
273
|
+
Interactive wizard to create custom personas.
|
|
274
|
+
|
|
275
|
+
[Stub - Coming soon]
|
|
276
|
+
"""
|
|
277
|
+
from crowdmind.validate.personas import PERSONA_PACKS, Persona
|
|
278
|
+
|
|
279
|
+
console.print(Panel.fit(
|
|
280
|
+
"[bold blue]CrowdMind Personas[/bold blue]\n"
|
|
281
|
+
"Interactive persona creation wizard",
|
|
282
|
+
border_style="blue"
|
|
283
|
+
))
|
|
284
|
+
|
|
285
|
+
console.print("\n[bold]Available Persona Packs:[/bold]")
|
|
286
|
+
for name, pack in PERSONA_PACKS.items():
|
|
287
|
+
console.print(f" [cyan]{name}[/cyan]: {pack.description} ({len(pack.personas)} personas)")
|
|
288
|
+
|
|
289
|
+
console.print("\n[yellow]Interactive wizard coming soon![/yellow]")
|
|
290
|
+
console.print("For now, use --pack option with validate/analyze commands.")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.command()
|
|
294
|
+
def demo():
|
|
295
|
+
"""
|
|
296
|
+
Show a demo of CrowdMind capabilities.
|
|
297
|
+
|
|
298
|
+
[Stub - Coming soon]
|
|
299
|
+
"""
|
|
300
|
+
console.print(Panel.fit(
|
|
301
|
+
"[bold blue]CrowdMind Demo[/bold blue]\n"
|
|
302
|
+
"Interactive demonstration",
|
|
303
|
+
border_style="blue"
|
|
304
|
+
))
|
|
305
|
+
|
|
306
|
+
console.print("\n[yellow]Demo mode coming soon![/yellow]")
|
|
307
|
+
console.print("\nTry these commands instead:")
|
|
308
|
+
console.print(" [cyan]crowdmind validate \"My SaaS idea description\"[/cyan]")
|
|
309
|
+
console.print(" [cyan]crowdmind research --sources reddit hackernews[/cyan]")
|
|
310
|
+
console.print(" [cyan]crowdmind analyze ./my-project[/cyan]")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@app.command()
|
|
314
|
+
def config():
|
|
315
|
+
"""
|
|
316
|
+
Setup wizard for CrowdMind configuration.
|
|
317
|
+
|
|
318
|
+
[Stub - Coming soon]
|
|
319
|
+
"""
|
|
320
|
+
from crowdmind.config import CONFIG_FILE, get_config
|
|
321
|
+
|
|
322
|
+
console.print(Panel.fit(
|
|
323
|
+
"[bold blue]CrowdMind Configuration[/bold blue]",
|
|
324
|
+
border_style="blue"
|
|
325
|
+
))
|
|
326
|
+
|
|
327
|
+
cfg = get_config()
|
|
328
|
+
|
|
329
|
+
console.print(f"\n[bold]Config file:[/bold] {CONFIG_FILE}")
|
|
330
|
+
console.print(f"[bold]Config exists:[/bold] {CONFIG_FILE.exists()}")
|
|
331
|
+
|
|
332
|
+
console.print("\n[bold]Current Settings:[/bold]")
|
|
333
|
+
console.print(f" Default model: {cfg.default_model}")
|
|
334
|
+
console.print(f" Default personas: {cfg.default_personas}")
|
|
335
|
+
console.print(f" Default provider: {cfg.default_provider}")
|
|
336
|
+
console.print(f" Cache dir: {cfg.cache_dir}")
|
|
337
|
+
|
|
338
|
+
console.print("\n[bold]API Keys:[/bold]")
|
|
339
|
+
console.print(f" OpenAI: {'[green]configured[/green]' if cfg.openai_api_key else '[red]not set[/red]'}")
|
|
340
|
+
console.print(f" Anthropic: {'[green]configured[/green]' if cfg.anthropic_api_key else '[red]not set[/red]'}")
|
|
341
|
+
console.print(f" Google: {'[green]configured[/green]' if cfg.google_api_key else '[red]not set[/red]'}")
|
|
342
|
+
console.print(f" Groq: {'[green]configured[/green]' if cfg.groq_api_key else '[red]not set[/red]'}")
|
|
343
|
+
console.print(f" GitHub: {'[green]configured[/green]' if cfg.github_token else '[red]not set[/red]'}")
|
|
344
|
+
|
|
345
|
+
console.print("\n[yellow]Interactive setup wizard coming soon![/yellow]")
|
|
346
|
+
console.print("\nFor now, set environment variables:")
|
|
347
|
+
console.print(" export ANTHROPIC_API_KEY=your-key")
|
|
348
|
+
console.print(" export CROWDMIND_MODEL=claude-sonnet-4-6")
|
|
349
|
+
console.print(" export CROWDMIND_PERSONAS=15")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.callback()
|
|
353
|
+
def main_callback(
|
|
354
|
+
version: bool = typer.Option(False, "--version", "-V", help="Show version"),
|
|
355
|
+
):
|
|
356
|
+
"""
|
|
357
|
+
CrowdMind - AI-Powered Research & Validation for Products
|
|
358
|
+
"""
|
|
359
|
+
if version:
|
|
360
|
+
from crowdmind import __version__
|
|
361
|
+
console.print(f"crowdmind version {__version__}")
|
|
362
|
+
raise typer.Exit()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def main():
|
|
366
|
+
"""Entry point for the CLI."""
|
|
367
|
+
app()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
main()
|
crowdmind/config.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrowdMind Configuration
|
|
3
|
+
|
|
4
|
+
Loads configuration from:
|
|
5
|
+
1. ~/.crowdmind/config.yaml
|
|
6
|
+
2. Environment variables
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import yaml
|
|
16
|
+
HAS_YAML = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_YAML = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
CONFIG_DIR = Path.home() / ".crowdmind"
|
|
22
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CrowdMindConfig:
|
|
27
|
+
"""Configuration for CrowdMind."""
|
|
28
|
+
|
|
29
|
+
# API Keys
|
|
30
|
+
openai_api_key: Optional[str] = None
|
|
31
|
+
anthropic_api_key: Optional[str] = None
|
|
32
|
+
google_api_key: Optional[str] = None
|
|
33
|
+
groq_api_key: Optional[str] = None
|
|
34
|
+
github_token: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
# Default settings
|
|
37
|
+
default_model: str = "claude-sonnet-4-6"
|
|
38
|
+
default_personas: int = 10
|
|
39
|
+
default_provider: str = "anthropic"
|
|
40
|
+
|
|
41
|
+
# Cache settings
|
|
42
|
+
cache_dir: Path = field(default_factory=lambda: CONFIG_DIR / "cache")
|
|
43
|
+
cache_hours: int = 12
|
|
44
|
+
|
|
45
|
+
# Output settings
|
|
46
|
+
output_dir: Path = field(default_factory=lambda: Path.cwd() / "crowdmind_output")
|
|
47
|
+
|
|
48
|
+
# Research settings
|
|
49
|
+
reddit_subreddits: list = field(default_factory=lambda: [
|
|
50
|
+
"programming", "webdev", "devops", "startups", "SaaS"
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def load(cls) -> "CrowdMindConfig":
|
|
55
|
+
"""Load configuration from file and environment variables."""
|
|
56
|
+
config = cls()
|
|
57
|
+
|
|
58
|
+
# Load from YAML if available
|
|
59
|
+
if HAS_YAML and CONFIG_FILE.exists():
|
|
60
|
+
try:
|
|
61
|
+
with open(CONFIG_FILE) as f:
|
|
62
|
+
yaml_config = yaml.safe_load(f) or {}
|
|
63
|
+
config = cls._from_dict(yaml_config)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# Override with environment variables
|
|
68
|
+
config.openai_api_key = os.getenv("OPENAI_API_KEY", config.openai_api_key)
|
|
69
|
+
config.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY", config.anthropic_api_key)
|
|
70
|
+
config.google_api_key = os.getenv("GOOGLE_API_KEY", config.google_api_key)
|
|
71
|
+
config.groq_api_key = os.getenv("GROQ_API_KEY", config.groq_api_key)
|
|
72
|
+
config.github_token = os.getenv("GITHUB_TOKEN", config.github_token)
|
|
73
|
+
|
|
74
|
+
# CrowdMind-specific env vars
|
|
75
|
+
if os.getenv("CROWDMIND_MODEL"):
|
|
76
|
+
config.default_model = os.getenv("CROWDMIND_MODEL")
|
|
77
|
+
if os.getenv("CROWDMIND_PERSONAS"):
|
|
78
|
+
try:
|
|
79
|
+
config.default_personas = int(os.getenv("CROWDMIND_PERSONAS"))
|
|
80
|
+
except ValueError:
|
|
81
|
+
pass
|
|
82
|
+
if os.getenv("CROWDMIND_PROVIDER"):
|
|
83
|
+
config.default_provider = os.getenv("CROWDMIND_PROVIDER")
|
|
84
|
+
|
|
85
|
+
return config
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def _from_dict(cls, data: Dict[str, Any]) -> "CrowdMindConfig":
|
|
89
|
+
"""Create config from dictionary."""
|
|
90
|
+
config = cls()
|
|
91
|
+
|
|
92
|
+
# API keys
|
|
93
|
+
api_keys = data.get("api_keys", {})
|
|
94
|
+
config.openai_api_key = api_keys.get("openai")
|
|
95
|
+
config.anthropic_api_key = api_keys.get("anthropic")
|
|
96
|
+
config.google_api_key = api_keys.get("google")
|
|
97
|
+
config.groq_api_key = api_keys.get("groq")
|
|
98
|
+
config.github_token = api_keys.get("github")
|
|
99
|
+
|
|
100
|
+
# Defaults
|
|
101
|
+
defaults = data.get("defaults", {})
|
|
102
|
+
config.default_model = defaults.get("model", config.default_model)
|
|
103
|
+
config.default_personas = defaults.get("personas", config.default_personas)
|
|
104
|
+
config.default_provider = defaults.get("provider", config.default_provider)
|
|
105
|
+
|
|
106
|
+
# Cache
|
|
107
|
+
cache = data.get("cache", {})
|
|
108
|
+
if cache.get("dir"):
|
|
109
|
+
config.cache_dir = Path(cache["dir"]).expanduser()
|
|
110
|
+
config.cache_hours = cache.get("hours", config.cache_hours)
|
|
111
|
+
|
|
112
|
+
# Output
|
|
113
|
+
if data.get("output_dir"):
|
|
114
|
+
config.output_dir = Path(data["output_dir"]).expanduser()
|
|
115
|
+
|
|
116
|
+
# Research
|
|
117
|
+
research = data.get("research", {})
|
|
118
|
+
if research.get("subreddits"):
|
|
119
|
+
config.reddit_subreddits = research["subreddits"]
|
|
120
|
+
|
|
121
|
+
return config
|
|
122
|
+
|
|
123
|
+
def save(self) -> None:
|
|
124
|
+
"""Save configuration to YAML file."""
|
|
125
|
+
if not HAS_YAML:
|
|
126
|
+
raise ImportError("PyYAML is required to save config. Install with: pip install pyyaml")
|
|
127
|
+
|
|
128
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
data = {
|
|
131
|
+
"api_keys": {
|
|
132
|
+
"openai": self.openai_api_key,
|
|
133
|
+
"anthropic": self.anthropic_api_key,
|
|
134
|
+
"google": self.google_api_key,
|
|
135
|
+
"groq": self.groq_api_key,
|
|
136
|
+
"github": self.github_token,
|
|
137
|
+
},
|
|
138
|
+
"defaults": {
|
|
139
|
+
"model": self.default_model,
|
|
140
|
+
"personas": self.default_personas,
|
|
141
|
+
"provider": self.default_provider,
|
|
142
|
+
},
|
|
143
|
+
"cache": {
|
|
144
|
+
"dir": str(self.cache_dir),
|
|
145
|
+
"hours": self.cache_hours,
|
|
146
|
+
},
|
|
147
|
+
"output_dir": str(self.output_dir),
|
|
148
|
+
"research": {
|
|
149
|
+
"subreddits": self.reddit_subreddits,
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
with open(CONFIG_FILE, "w") as f:
|
|
154
|
+
yaml.dump(data, f, default_flow_style=False)
|
|
155
|
+
|
|
156
|
+
def ensure_dirs(self) -> None:
|
|
157
|
+
"""Ensure required directories exist."""
|
|
158
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_config() -> CrowdMindConfig:
|
|
163
|
+
"""Get the current configuration."""
|
|
164
|
+
return CrowdMindConfig.load()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_api_key(provider: str) -> Optional[str]:
|
|
168
|
+
"""Get API key for a provider."""
|
|
169
|
+
config = get_config()
|
|
170
|
+
key_map = {
|
|
171
|
+
"openai": config.openai_api_key,
|
|
172
|
+
"anthropic": config.anthropic_api_key,
|
|
173
|
+
"google": config.google_api_key,
|
|
174
|
+
"groq": config.groq_api_key,
|
|
175
|
+
"github": config.github_token,
|
|
176
|
+
}
|
|
177
|
+
return key_map.get(provider.lower())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrowdMind Ideate Module
|
|
3
|
+
|
|
4
|
+
Generate feature ideas based on research and codebase analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from crowdmind.ideate.features import run_ideation, generate_feature_ideas, get_top_ideas
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"run_ideation",
|
|
11
|
+
"generate_feature_ideas",
|
|
12
|
+
"get_top_ideas",
|
|
13
|
+
]
|