From d014f2e0b6475b2eed837d6ddd8da9aaa3a0779b Mon Sep 17 00:00:00 2001 From: JT Wei Date: Sun, 5 Apr 2026 18:26:12 +0800 Subject: [PATCH 01/17] feat(idea): add exportable assessment reports in markdown and json --- README.md | 11 ++++++ README.zh-CN.md | 11 ++++++ src/api/routes/idea.py | 82 +++++++++++++++++++++++++++++++++++++++--- src/models/schemas.py | 6 ++++ tests/test_api.py | 36 +++++++++++++++++++ 5 files changed, 142 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1332f34..c13a1e8 100644 --- a/README.md +++ b/README.md @@ -193,11 +193,22 @@ curl -X POST http://localhost:8000/api/v1/idea/assess \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant for startup teams", "product_doc": "Need repo indexing, recommendation, and integration guidance"}' +# Export assessment report as Markdown +curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=markdown" \ + -H "Content-Type: application/json" \ + -d '{"idea": "AI coding workflow assistant for startup teams", "product_doc": "Need repo indexing, recommendation, and integration guidance"}' + +# Export assessment report as JSON envelope +curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ + -H "Content-Type: application/json" \ + -d '{"idea": "AI coding workflow assistant for startup teams", "product_doc": "Need repo indexing, recommendation, and integration guidance"}' + # Response includes: # - verdict + existing_project_probability # - action_recommendation (build|fork|integrate) + action_rationale # - decision_signals for explainability # - similar_projects with evidence_snippets +# - export endpoint supports markdown/json report output ``` Full API documentation available at `http://localhost:8000/docs` (Swagger UI) or `http://localhost:8000/redoc` (ReDoc). diff --git a/README.zh-CN.md b/README.zh-CN.md index e53bb62..d7a8054 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -146,6 +146,16 @@ curl -X POST http://localhost:8000/api/v1/search \ curl -X POST http://localhost:8000/api/v1/idea/assess \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant", "product_doc": "Need repo indexing and recommendation"}' + +# 导出 Markdown 报告 +curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=markdown" \ + -H "Content-Type: application/json" \ + -d '{"idea": "AI coding workflow assistant", "product_doc": "Need repo indexing and recommendation"}' + +# 导出 JSON 报告 +curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ + -H "Content-Type: application/json" \ + -d '{"idea": "AI coding workflow assistant", "product_doc": "Need repo indexing and recommendation"}' ``` 响应包含: @@ -153,6 +163,7 @@ curl -X POST http://localhost:8000/api/v1/idea/assess \ - action_recommendation(build/fork/integrate) - decision_signals(判定依据) - similar_projects 与 evidence_snippets +- 导出接口支持 markdown/json 两种报告格式 ## 自托管 diff --git a/src/api/routes/idea.py b/src/api/routes/idea.py index e98a333..17ecc4b 100644 --- a/src/api/routes/idea.py +++ b/src/api/routes/idea.py @@ -8,7 +8,8 @@ import re -from fastapi import APIRouter +from fastapi import APIRouter, Query +from fastapi.responses import PlainTextResponse from src.core.llm.adapter import llm from src.core.llm.router import CallLevel @@ -17,6 +18,7 @@ DecisionSignals, DifferentiationOpportunity, IdeaAssessCandidate, + IdeaAssessExportResponse, IdeaAssessRequest, IdeaAssessResponse, ) @@ -175,9 +177,60 @@ def _action_recommendation(verdict: str, probability: float) -> tuple[str, str]: ) -@router.post("/idea/assess", response_model=IdeaAssessResponse) -async def assess_idea(request: IdeaAssessRequest) -> IdeaAssessResponse: - """Assess if an idea is already implemented and suggest similar projects.""" +def _render_markdown_report(assessment: IdeaAssessResponse) -> str: + lines: list[str] = [ + "# Idea Assessment Report", + "", + "## Input", + f"- Idea: {assessment.idea}", + "", + "## Verdict", + f"- Verdict: {assessment.verdict}", + f"- Confidence: {assessment.confidence}", + f"- Existing project probability: {assessment.existing_project_probability}", + f"- Recommended action: {assessment.action_recommendation}", + f"- Action rationale: {assessment.action_rationale}", + "", + "## Summary", + assessment.summary, + "", + "## Decision Signals", + f"- Candidate count: {assessment.decision_signals.candidate_count}", + f"- Top score: {assessment.decision_signals.top_score}", + f"- Base probability: {assessment.decision_signals.base_probability}", + f"- Calibrated probability: {assessment.decision_signals.calibrated_probability}", + f"- Avg keyword hits: {assessment.decision_signals.avg_keyword_hits}", + f"- Non-empty reason ratio: {assessment.decision_signals.nonempty_reason_ratio}", + "", + "## Similar Projects", + ] + + if assessment.similar_projects: + for item in assessment.similar_projects: + lines.append( + f"- {item.rank}. {item.project} ({item.source or 'unknown'}, score={item.score})" + ) + lines.append(f" - Match reason: {item.match_reason}") + if item.evidence_snippets: + lines.append(f" - Evidence: {' | '.join(item.evidence_snippets)}") + else: + lines.append("- No close matches found") + + lines.extend(["", "## Differentiation Opportunities"]) + for opp in assessment.differentiation_opportunities: + lines.append(f"- [{opp.category}] {opp.opportunity}") + lines.append(f" - Rationale: {opp.rationale}") + lines.append(f" - Effort: {opp.implementation_effort}") + + lines.extend(["", "## Next Actions"]) + for action in assessment.next_actions: + lines.append(f"- {action}") + + return "\n".join(lines) + + +async def _assess_core(request: IdeaAssessRequest) -> IdeaAssessResponse: + """Core idea assessment logic shared by standard and export endpoints.""" combined_query = request.idea.strip() if request.product_doc: combined_query += "\n\n" + request.product_doc.strip() @@ -309,3 +362,24 @@ async def assess_idea(request: IdeaAssessRequest) -> IdeaAssessResponse: differentiation_opportunities=opportunities, next_actions=next_actions, ) + + +@router.post("/idea/assess", response_model=IdeaAssessResponse) +async def assess_idea(request: IdeaAssessRequest) -> IdeaAssessResponse: + """Assess if an idea is already implemented and suggest similar projects.""" + return await _assess_core(request) + + +@router.post("/idea/assess/export", response_model=IdeaAssessExportResponse) +async def export_assessment_report( + request: IdeaAssessRequest, + format: str = Query("markdown", pattern="^(markdown|json)$"), +) -> IdeaAssessExportResponse | PlainTextResponse: + """Export idea assessment as Markdown or JSON report.""" + assessment = await _assess_core(request) + + if format == "markdown": + markdown = _render_markdown_report(assessment) + return PlainTextResponse(content=markdown, media_type="text/markdown") + + return IdeaAssessExportResponse(report_type="json", assessment=assessment) diff --git a/src/models/schemas.py b/src/models/schemas.py index b149cc4..b430ac6 100644 --- a/src/models/schemas.py +++ b/src/models/schemas.py @@ -123,6 +123,12 @@ class IdeaAssessResponse(BaseModel): generated_at: datetime = Field(default_factory=datetime.utcnow) +class IdeaAssessExportResponse(BaseModel): + report_type: str # markdown|json + assessment: IdeaAssessResponse + generated_at: datetime = Field(default_factory=datetime.utcnow) + + # ── Evaluation ─────────────────────────────────────────────────────────────── diff --git a/tests/test_api.py b/tests/test_api.py index 916b983..a77d537 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -103,3 +103,39 @@ async def test_assess_idea_endpoint(client: AsyncClient): assert "calibrated_probability" in data["decision_signals"] if data["similar_projects"]: assert "evidence_snippets" in data["similar_projects"][0] + + +@pytest.mark.asyncio +async def test_assess_idea_export_markdown(client: AsyncClient): + resp = await client.post( + "/api/v1/idea/assess/export?format=markdown", + json={ + "idea": "Build an open-source tool to compare similar Python web frameworks", + "product_doc": "Need semantic search and recommendation quality scoring.", + }, + ) + assert resp.status_code == 200 + assert resp.headers.get("content-type", "").startswith("text/markdown") + body = resp.text + assert "# Idea Assessment Report" in body + assert "## Verdict" in body + + +@pytest.mark.asyncio +async def test_assess_idea_export_json(client: AsyncClient): + resp = await client.post( + "/api/v1/idea/assess/export?format=json", + json={ + "idea": "Build an open-source tool to compare similar Python web frameworks", + "product_doc": "Need semantic search and recommendation quality scoring.", + }, + ) + assert resp.status_code == 200 + data = resp.json() + assert data["report_type"] == "json" + assert "assessment" in data + assert data["assessment"]["verdict"] in { + "already_exists", + "similar_projects_exist", + "likely_novel", + } From 48ed87c89fb94cf6a99734fd013354cd1ecbc73e Mon Sep 17 00:00:00 2001 From: JT Wei Date: Sun, 5 Apr 2026 22:04:39 +0800 Subject: [PATCH 02/17] feat(idea): add batch assessment api with verdict aggregation --- README.md | 6 ++++++ README.zh-CN.md | 6 ++++++ src/api/routes/idea.py | 32 ++++++++++++++++++++++++++++++++ src/models/schemas.py | 22 ++++++++++++++++++++++ tests/test_api.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+) diff --git a/README.md b/README.md index c13a1e8..13405d1 100644 --- a/README.md +++ b/README.md @@ -203,12 +203,18 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant for startup teams", "product_doc": "Need repo indexing, recommendation, and integration guidance"}' +# Batch assess multiple ideas +curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ + -H "Content-Type: application/json" \ + -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6}' + # Response includes: # - verdict + existing_project_probability # - action_recommendation (build|fork|integrate) + action_rationale # - decision_signals for explainability # - similar_projects with evidence_snippets # - export endpoint supports markdown/json report output +# - batch endpoint returns per-idea results + verdict counts ``` Full API documentation available at `http://localhost:8000/docs` (Swagger UI) or `http://localhost:8000/redoc` (ReDoc). diff --git a/README.zh-CN.md b/README.zh-CN.md index d7a8054..7eeb3dc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -156,6 +156,11 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=markdown" \ curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant", "product_doc": "Need repo indexing and recommendation"}' + +# 批量评估多个想法 +curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ + -H "Content-Type: application/json" \ + -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6}' ``` 响应包含: @@ -164,6 +169,7 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ - decision_signals(判定依据) - similar_projects 与 evidence_snippets - 导出接口支持 markdown/json 两种报告格式 +- 批量接口返回逐条结果和 verdict 统计 ## 自托管 diff --git a/src/api/routes/idea.py b/src/api/routes/idea.py index 17ecc4b..acf60af 100644 --- a/src/api/routes/idea.py +++ b/src/api/routes/idea.py @@ -17,6 +17,8 @@ from src.models.schemas import ( DecisionSignals, DifferentiationOpportunity, + IdeaAssessBatchRequest, + IdeaAssessBatchResponse, IdeaAssessCandidate, IdeaAssessExportResponse, IdeaAssessRequest, @@ -383,3 +385,33 @@ async def export_assessment_report( return PlainTextResponse(content=markdown, media_type="text/markdown") return IdeaAssessExportResponse(report_type="json", assessment=assessment) + + +@router.post("/idea/assess/batch", response_model=IdeaAssessBatchResponse) +async def assess_idea_batch(request: IdeaAssessBatchRequest) -> IdeaAssessBatchResponse: + """Assess multiple ideas/PRDs in one request.""" + results: list[IdeaAssessResponse] = [] + counters = { + "already_exists": 0, + "similar_projects_exist": 0, + "likely_novel": 0, + } + + for item in request.items: + single_request = IdeaAssessRequest( + idea=item.idea, + product_doc=item.product_doc, + limit=request.limit, + ) + assessment = await _assess_core(single_request) + results.append(assessment) + if assessment.verdict in counters: + counters[assessment.verdict] += 1 + + return IdeaAssessBatchResponse( + total=len(results), + already_exists=counters["already_exists"], + similar_projects_exist=counters["similar_projects_exist"], + likely_novel=counters["likely_novel"], + results=results, + ) diff --git a/src/models/schemas.py b/src/models/schemas.py index b430ac6..990d07a 100644 --- a/src/models/schemas.py +++ b/src/models/schemas.py @@ -129,6 +129,28 @@ class IdeaAssessExportResponse(BaseModel): generated_at: datetime = Field(default_factory=datetime.utcnow) +class IdeaAssessBatchItem(BaseModel): + idea: str = Field(..., description="Core product idea or one-line concept") + product_doc: str | None = Field( + None, + description="Optional detailed PRD/brief for deeper similarity analysis", + ) + + +class IdeaAssessBatchRequest(BaseModel): + items: list[IdeaAssessBatchItem] = Field(..., min_length=1, max_length=20) + limit: int = Field(8, ge=3, le=20, description="How many similar projects to inspect for each idea") + + +class IdeaAssessBatchResponse(BaseModel): + total: int + already_exists: int + similar_projects_exist: int + likely_novel: int + results: list[IdeaAssessResponse] = Field(default_factory=list) + generated_at: datetime = Field(default_factory=datetime.utcnow) + + # ── Evaluation ─────────────────────────────────────────────────────────────── diff --git a/tests/test_api.py b/tests/test_api.py index a77d537..be68ce7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -139,3 +139,33 @@ async def test_assess_idea_export_json(client: AsyncClient): "similar_projects_exist", "likely_novel", } + + +@pytest.mark.asyncio +async def test_assess_idea_batch(client: AsyncClient): + resp = await client.post( + "/api/v1/idea/assess/batch", + json={ + "items": [ + { + "idea": "Open-source API mocking tool for integration tests", + "product_doc": "Need route templates and scenario replay.", + }, + { + "idea": "Collaborative PR review assistant for OSS maintainers", + "product_doc": "Need triage automation and contributor onboarding hints.", + }, + ], + "limit": 6, + }, + ) + assert resp.status_code == 200 + data = resp.json() + assert data["total"] == 2 + assert len(data["results"]) == 2 + verdict_sum = ( + data["already_exists"] + + data["similar_projects_exist"] + + data["likely_novel"] + ) + assert verdict_sum == 2 From 3007dd662480db887d2cd66d981f1f500bd068ed Mon Sep 17 00:00:00 2001 From: JT Wei Date: Sun, 5 Apr 2026 22:20:12 +0800 Subject: [PATCH 03/17] feat(idea): add concurrent batch assessment with timeout controls --- README.md | 3 +- README.zh-CN.md | 3 +- src/api/routes/idea.py | 85 +++++++++++++++++++++++++++++++++++++----- src/models/schemas.py | 7 ++++ tests/test_api.py | 2 + 5 files changed, 89 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 13405d1..a5550fe 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ # Batch assess multiple ideas curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ -H "Content-Type: application/json" \ - -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6}' + -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6,"max_concurrency":2,"per_item_timeout_seconds":30}' # Response includes: # - verdict + existing_project_probability @@ -215,6 +215,7 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ # - similar_projects with evidence_snippets # - export endpoint supports markdown/json report output # - batch endpoint returns per-idea results + verdict counts +# - batch supports max_concurrency and per_item_timeout_seconds ``` Full API documentation available at `http://localhost:8000/docs` (Swagger UI) or `http://localhost:8000/redoc` (ReDoc). diff --git a/README.zh-CN.md b/README.zh-CN.md index 7eeb3dc..aa5a4e9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -160,7 +160,7 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ # 批量评估多个想法 curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ -H "Content-Type: application/json" \ - -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6}' + -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6,"max_concurrency":2,"per_item_timeout_seconds":30}' ``` 响应包含: @@ -170,6 +170,7 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ - similar_projects 与 evidence_snippets - 导出接口支持 markdown/json 两种报告格式 - 批量接口返回逐条结果和 verdict 统计 +- 批量接口支持 max_concurrency 与 per_item_timeout_seconds ## 自托管 diff --git a/src/api/routes/idea.py b/src/api/routes/idea.py index acf60af..b063ab3 100644 --- a/src/api/routes/idea.py +++ b/src/api/routes/idea.py @@ -6,7 +6,9 @@ from __future__ import annotations +import asyncio import re +from contextlib import suppress from fastapi import APIRouter, Query from fastapi.responses import PlainTextResponse @@ -390,21 +392,86 @@ async def export_assessment_report( @router.post("/idea/assess/batch", response_model=IdeaAssessBatchResponse) async def assess_idea_batch(request: IdeaAssessBatchRequest) -> IdeaAssessBatchResponse: """Assess multiple ideas/PRDs in one request.""" - results: list[IdeaAssessResponse] = [] - counters = { - "already_exists": 0, - "similar_projects_exist": 0, - "likely_novel": 0, - } + semaphore = asyncio.Semaphore(request.max_concurrency) - for item in request.items: + async def _assess_one(item) -> IdeaAssessResponse: single_request = IdeaAssessRequest( idea=item.idea, product_doc=item.product_doc, limit=request.limit, ) - assessment = await _assess_core(single_request) - results.append(assessment) + + task: asyncio.Task[IdeaAssessResponse] | None = None + try: + async with semaphore: + task = asyncio.create_task(_assess_core(single_request)) + return await asyncio.wait_for( + task, + timeout=request.per_item_timeout_seconds, + ) + except asyncio.TimeoutError: + if task is not None: + task.cancel() + with suppress(asyncio.CancelledError, Exception): + await task + return IdeaAssessResponse( + idea=item.idea, + verdict="likely_novel", + confidence="low", + existing_project_probability=0.0, + action_recommendation="build", + action_rationale="Assessment timed out; retry with fewer items or higher timeout.", + decision_signals=DecisionSignals( + candidate_count=0, + top_score=0.0, + base_probability=0.0, + calibrated_probability=0.0, + avg_keyword_hits=0.0, + nonempty_reason_ratio=0.0, + ), + summary="Assessment timed out before retrieval and scoring completed.", + similar_projects=[], + differentiation_opportunities=_build_opportunities("likely_novel"), + next_actions=[ + "Retry batch assessment with a larger per_item_timeout_seconds.", + "Reduce batch size to improve per-item completion speed.", + ], + ) + except Exception: + return IdeaAssessResponse( + idea=item.idea, + verdict="likely_novel", + confidence="low", + existing_project_probability=0.0, + action_recommendation="build", + action_rationale="Assessment encountered an internal error; please retry.", + decision_signals=DecisionSignals( + candidate_count=0, + top_score=0.0, + base_probability=0.0, + calibrated_probability=0.0, + avg_keyword_hits=0.0, + nonempty_reason_ratio=0.0, + ), + summary="Assessment failed due to an internal processing error.", + similar_projects=[], + differentiation_opportunities=_build_opportunities("likely_novel"), + next_actions=[ + "Retry the request.", + "If failures persist, inspect service logs and provider connectivity.", + ], + ) + + tasks = [_assess_one(item) for item in request.items] + results = await asyncio.gather(*tasks) + + counters = { + "already_exists": 0, + "similar_projects_exist": 0, + "likely_novel": 0, + } + + for assessment in results: if assessment.verdict in counters: counters[assessment.verdict] += 1 diff --git a/src/models/schemas.py b/src/models/schemas.py index 990d07a..ad24967 100644 --- a/src/models/schemas.py +++ b/src/models/schemas.py @@ -140,6 +140,13 @@ class IdeaAssessBatchItem(BaseModel): class IdeaAssessBatchRequest(BaseModel): items: list[IdeaAssessBatchItem] = Field(..., min_length=1, max_length=20) limit: int = Field(8, ge=3, le=20, description="How many similar projects to inspect for each idea") + max_concurrency: int = Field(4, ge=1, le=10, description="Max concurrent assessments in a batch") + per_item_timeout_seconds: float = Field( + 25.0, + ge=5.0, + le=120.0, + description="Timeout for each single idea assessment", + ) class IdeaAssessBatchResponse(BaseModel): diff --git a/tests/test_api.py b/tests/test_api.py index be68ce7..0e0cec6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -157,6 +157,8 @@ async def test_assess_idea_batch(client: AsyncClient): }, ], "limit": 6, + "max_concurrency": 2, + "per_item_timeout_seconds": 30, }, ) assert resp.status_code == 200 From fdea2799c465754888b1240d48074383a0ad2e63 Mon Sep 17 00:00:00 2001 From: JT Wei Date: Sun, 5 Apr 2026 22:42:13 +0800 Subject: [PATCH 04/17] feat(local-dev): move test ports and add batch idea assessment UI --- README.md | 20 +++--- README.zh-CN.md | 16 ++--- docker-compose.yml | 7 +- docs/self-hosting.md | 12 ++-- scripts/release_check.sh | 20 +++--- web/next.config.js | 4 +- web/package.json | 4 +- web/src/app/idea/page.tsx | 143 +++++++++++++++++++++++++++++++++++++- web/src/lib/api.ts | 14 ++++ 9 files changed, 201 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index a5550fe..aab8b80 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ docker compose up -d Important: Owlscope does not provide shared AI provider keys. You must supply your own API keys for LLM-powered features. -The Web App will be available at `http://localhost:3000` and the API at `http://localhost:8000`. +The Web App will be available at `http://127.0.0.1:3100` and the API at `http://127.0.0.1:8010`. ### Local Development @@ -121,7 +121,7 @@ DEEPSEEK_API_KEY=your_deepseek_key EOF # Start the API server -uvicorn src.api.main:app --reload --port 8000 +uvicorn src.api.main:app --reload --host 127.0.0.1 --port 8010 ``` #### Web App (Next.js) @@ -176,35 +176,35 @@ Notes: ```bash # Search -curl -X POST http://localhost:8000/api/v1/search \ +curl -X POST http://127.0.0.1:8010/api/v1/search \ -H "Content-Type: application/json" \ -d '{"query": "lightweight Python web framework"}' # Evaluate a project -curl http://localhost:8000/api/v1/evaluate/github:library:encode/httpx +curl http://127.0.0.1:8010/api/v1/evaluate/github:library:encode/httpx # Compare projects -curl -X POST http://localhost:8000/api/v1/compare \ +curl -X POST http://127.0.0.1:8010/api/v1/compare \ -H "Content-Type: application/json" \ -d '{"projects": ["github:library:fastapi/fastapi", "github:library:pallets/flask", "github:library:django/django"]}' # Assess whether an idea is already implemented -curl -X POST http://localhost:8000/api/v1/idea/assess \ +curl -X POST http://127.0.0.1:8010/api/v1/idea/assess \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant for startup teams", "product_doc": "Need repo indexing, recommendation, and integration guidance"}' # Export assessment report as Markdown -curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=markdown" \ +curl -X POST "http://127.0.0.1:8010/api/v1/idea/assess/export?format=markdown" \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant for startup teams", "product_doc": "Need repo indexing, recommendation, and integration guidance"}' # Export assessment report as JSON envelope -curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ +curl -X POST "http://127.0.0.1:8010/api/v1/idea/assess/export?format=json" \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant for startup teams", "product_doc": "Need repo indexing, recommendation, and integration guidance"}' # Batch assess multiple ideas -curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ +curl -X POST "http://127.0.0.1:8010/api/v1/idea/assess/batch" \ -H "Content-Type: application/json" \ -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6,"max_concurrency":2,"per_item_timeout_seconds":30}' @@ -218,7 +218,7 @@ curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ # - batch supports max_concurrency and per_item_timeout_seconds ``` -Full API documentation available at `http://localhost:8000/docs` (Swagger UI) or `http://localhost:8000/redoc` (ReDoc). +Full API documentation available at `http://127.0.0.1:8010/docs` (Swagger UI) or `http://127.0.0.1:8010/redoc` (ReDoc). ### MCP Protocol diff --git a/README.zh-CN.md b/README.zh-CN.md index aa5a4e9..e965b0c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -74,8 +74,8 @@ docker compose up -d ``` 启动后: -- Web: http://localhost:3000 -- API: http://localhost:8000 +- Web: http://127.0.0.1:3100 +- API: http://127.0.0.1:8010 重要说明:Owlscope 不提供共享 AI Key。所有 LLM 功能都需要用户自行配置 API Key。 @@ -96,7 +96,7 @@ DEEPSEEK_API_KEY=your_deepseek_key # ANTHROPIC_API_KEY=your_anthropic_key EOF -uvicorn src.api.main:app --reload --port 8000 +uvicorn src.api.main:app --reload --host 127.0.0.1 --port 8010 ``` 前端: @@ -138,27 +138,27 @@ pytest -q -m integration tests/integration/test_compare_blackbox.py ```bash # 语义搜索 -curl -X POST http://localhost:8000/api/v1/search \ +curl -X POST http://127.0.0.1:8010/api/v1/search \ -H "Content-Type: application/json" \ -d '{"query": "lightweight Python web framework"}' # 想法评估(是否已有开源实现) -curl -X POST http://localhost:8000/api/v1/idea/assess \ +curl -X POST http://127.0.0.1:8010/api/v1/idea/assess \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant", "product_doc": "Need repo indexing and recommendation"}' # 导出 Markdown 报告 -curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=markdown" \ +curl -X POST "http://127.0.0.1:8010/api/v1/idea/assess/export?format=markdown" \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant", "product_doc": "Need repo indexing and recommendation"}' # 导出 JSON 报告 -curl -X POST "http://localhost:8000/api/v1/idea/assess/export?format=json" \ +curl -X POST "http://127.0.0.1:8010/api/v1/idea/assess/export?format=json" \ -H "Content-Type: application/json" \ -d '{"idea": "AI coding workflow assistant", "product_doc": "Need repo indexing and recommendation"}' # 批量评估多个想法 -curl -X POST "http://localhost:8000/api/v1/idea/assess/batch" \ +curl -X POST "http://127.0.0.1:8010/api/v1/idea/assess/batch" \ -H "Content-Type: application/json" \ -d '{"items":[{"idea":"Open-source API mocking tool","product_doc":"Need scenario replay"},{"idea":"PR review assistant for OSS maintainers","product_doc":"Need triage automation"}],"limit":6,"max_concurrency":2,"per_item_timeout_seconds":30}' ``` diff --git a/docker-compose.yml b/docker-compose.yml index 5566116..bdc35fe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: context: . dockerfile: docker/Dockerfile.api ports: - - "8000:8000" + - "8010:8000" env_file: - .env depends_on: @@ -26,11 +26,12 @@ services: context: ./web dockerfile: ../docker/Dockerfile.web ports: - - "3000:3000" + - "3100:3000" depends_on: - api environment: - - NEXT_PUBLIC_API_URL=http://api:8000 + - NEXT_PUBLIC_API_URL=http://127.0.0.1:8010 + - OWLSCOPE_API_ORIGIN=http://api:8000 volumes: - ./web/src:/app/src diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 4530536..c3e70a5 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -19,9 +19,9 @@ docker compose up -d ``` Services: -- Web: http://localhost:3000 -- API: http://localhost:8000 -- Swagger: http://localhost:8000/docs +- Web: http://127.0.0.1:3100 +- API: http://127.0.0.1:8010 +- Swagger: http://127.0.0.1:8010/docs ## Environment Variables @@ -42,7 +42,7 @@ Owlscope has deterministic fallbacks for some retrieval flows when model service python -m venv .venv source .venv/bin/activate pip install -e ".[dev]" -uvicorn src.api.main:app --reload --port 8000 +uvicorn src.api.main:app --reload --host 127.0.0.1 --port 8010 ``` ### Frontend @@ -56,8 +56,8 @@ npm run dev ## Health Checks ```bash -curl http://localhost:8000/health -curl http://localhost:8000/api/v1/admin/monitor +curl http://127.0.0.1:8010/health +curl http://127.0.0.1:8010/api/v1/admin/monitor ``` ## Data Notes diff --git a/scripts/release_check.sh b/scripts/release_check.sh index 9310179..01bc41c 100755 --- a/scripts/release_check.sh +++ b/scripts/release_check.sh @@ -4,6 +4,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT_DIR" +API_HOST="${OWLSCOPE_API_HOST:-127.0.0.1}" +API_PORT="${OWLSCOPE_API_PORT:-8010}" +API_BASE="http://${API_HOST}:${API_PORT}" + echo "[1/4] Running backend tests..." source .venv/bin/activate pytest -q -m "not integration" @@ -14,19 +18,19 @@ npm run build popd >/dev/null echo "[3/4] Starting API for smoke checks..." -lsof -ti:8000 | xargs kill -9 2>/dev/null || true -python -m uvicorn src.api.main:app --host 0.0.0.0 --port 8000 --log-level warning >/tmp/owlscope-release-check.log 2>&1 & +lsof -ti:"$API_PORT" | xargs kill -9 2>/dev/null || true +python -m uvicorn src.api.main:app --host "$API_HOST" --port "$API_PORT" --log-level warning >/tmp/owlscope-release-check.log 2>&1 & API_PID=$! trap 'kill "$API_PID" 2>/dev/null || true' EXIT for _ in {1..90}; do - if curl -fsS "http://localhost:8000/health" >/dev/null 2>&1; then + if curl -fsS "$API_BASE/health" >/dev/null 2>&1; then break fi sleep 1 done -if ! curl -fsS "http://localhost:8000/health" >/dev/null 2>&1; then +if ! curl -fsS "$API_BASE/health" >/dev/null 2>&1; then echo "API health check failed" echo "--- recent API logs ---" tail -n 80 /tmp/owlscope-release-check.log || true @@ -34,22 +38,22 @@ if ! curl -fsS "http://localhost:8000/health" >/dev/null 2>&1; then fi echo "[4/4] Smoke testing key endpoints..." -curl -fsS -X POST "http://localhost:8000/api/v1/search" \ +curl -fsS -X POST "$API_BASE/api/v1/search" \ -H "Content-Type: application/json" \ -d '{"query":"python web framework"}' >/dev/null -curl -fsS -X POST "http://localhost:8000/api/v1/idea/assess" \ +curl -fsS -X POST "$API_BASE/api/v1/idea/assess" \ -H "Content-Type: application/json" \ -d '{"idea":"Open-source assistant for startup idea validation"}' >/dev/null # Some environments may not have seeded comparison resources yet. -EVALUATE_CODE=$(curl -s -o /tmp/owlscope-evaluate.out -w "%{http_code}" "http://localhost:8000/api/v1/evaluate/github:library:encode/httpx") +EVALUATE_CODE=$(curl -s -o /tmp/owlscope-evaluate.out -w "%{http_code}" "$API_BASE/api/v1/evaluate/github:library:encode/httpx") if [[ "$EVALUATE_CODE" != "200" && "$EVALUATE_CODE" != "404" ]]; then echo "Unexpected /evaluate status: $EVALUATE_CODE" exit 1 fi -COMPARE_CODE=$(curl -s -o /tmp/owlscope-compare.out -w "%{http_code}" -X POST "http://localhost:8000/api/v1/compare" \ +COMPARE_CODE=$(curl -s -o /tmp/owlscope-compare.out -w "%{http_code}" -X POST "$API_BASE/api/v1/compare" \ -H "Content-Type: application/json" \ -d '{"projects":["github:library:fastapi/fastapi","github:library:pallets/flask"]}') if [[ "$COMPARE_CODE" != "200" && "$COMPARE_CODE" != "404" ]]; then diff --git a/web/next.config.js b/web/next.config.js index c5db160..d81681e 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -1,11 +1,13 @@ /** @type {import('next').NextConfig} */ +const apiOrigin = process.env.OWLSCOPE_API_ORIGIN || "http://127.0.0.1:8010"; + const nextConfig = { // API proxy to backend async rewrites() { return [ { source: "/api/:path*", - destination: "http://localhost:8000/api/:path*", + destination: `${apiOrigin}/api/:path*`, }, ]; }, diff --git a/web/package.json b/web/package.json index bb94abf..dbf3f2e 100644 --- a/web/package.json +++ b/web/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -H 127.0.0.1 -p 3100", "build": "next build", - "start": "next start", + "start": "next start -H 127.0.0.1 -p 3100", "lint": "next lint" }, "dependencies": { diff --git a/web/src/app/idea/page.tsx b/web/src/app/idea/page.tsx index 8e5d99e..4813de8 100644 --- a/web/src/app/idea/page.tsx +++ b/web/src/app/idea/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { assessIdea } from "@/lib/api"; +import { assessIdea, assessIdeaBatch } from "@/lib/api"; type Verdict = "already_exists" | "similar_projects_exist" | "likely_novel"; @@ -42,6 +42,14 @@ interface IdeaAssessResult { next_actions: string[]; } +interface IdeaAssessBatchResult { + total: number; + already_exists: number; + similar_projects_exist: number; + likely_novel: number; + results: IdeaAssessResult[]; +} + const VERDICT_UI: Record = { already_exists: { title: "Likely Already Implemented", @@ -65,6 +73,12 @@ export default function IdeaPage() { const [error, setError] = useState(""); const [copied, setCopied] = useState(false); const [result, setResult] = useState(null); + const [batchInput, setBatchInput] = useState(""); + const [batchLoading, setBatchLoading] = useState(false); + const [batchError, setBatchError] = useState(""); + const [batchResult, setBatchResult] = useState(null); + const [maxConcurrency, setMaxConcurrency] = useState(2); + const [timeoutSeconds, setTimeoutSeconds] = useState(30); const buildMarkdownReport = () => { if (!result) return ""; @@ -142,6 +156,45 @@ export default function IdeaPage() { } }; + const parseBatchItems = () => { + return batchInput + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const [ideaPart, docPart] = line.split("||", 2); + return { + idea: ideaPart.trim(), + product_doc: docPart?.trim() || undefined, + }; + }) + .filter((item) => item.idea.length > 0) + .slice(0, 20); + }; + + const runBatchAssessment = async () => { + const items = parseBatchItems(); + if (items.length === 0) return; + + setBatchLoading(true); + setBatchError(""); + setBatchResult(null); + + try { + const data = await assessIdeaBatch({ + items, + limit, + max_concurrency: maxConcurrency, + per_item_timeout_seconds: timeoutSeconds, + }); + setBatchResult(data); + } catch { + setBatchError("Failed to assess batch ideas. Please retry in a moment."); + } finally { + setBatchLoading(false); + } + }; + return (

💡 Idea Check

@@ -192,6 +245,52 @@ export default function IdeaPage() { {loading ? "Analyzing..." : "Assess Idea"} + +
+ +