Phase 1.4: 자율 연구 — by-project 엔드포인트 + ResearchPage + 세션 이력
백엔드 (crawler_platform/app/api/routes.py):
- POST /research/run/by-project 추가 — config_path 의존 제거
- ResearchRunByProjectRequest: project_name 기반, DB project.config 재구성
- GraphResearchLoop 동기 실행 (장시간 가능), asdict(result) 반환
- 기존 /projects/{n}/research/sessions, /research/sessions/{job_id}는 변경 없음
프론트엔드 API (src/lib/api/):
- research.ts: researchApi.startByProject/listSessions/getSession + Zod 스키마
- 결과 페이로드는 passthrough() (백엔드 dataclass 그대로 노출)
TanStack Query 훅 (src/hooks/):
- useResearch.ts: useStartResearch, useResearchSessions, useResearchSession
- queryKeys에 research.sessions, research.session 키
ResearchPage 신규 (src/pages/ResearchPage.tsx):
- 2열 레이아웃: 좌측 연구 설정 폼 + 우측 결과/이력
- 폼: 소스 선택, goal 텍스트(필수, 3~500자), 시드 URL(선택),
max_steps/max_branch/max_depth/min_relevance, same_domain_only
react-hook-form + zod 검증
- 결과 카드: 단계/페이지/엔티티/클레임 4종 stats + raw JSON details
- 세션 이력 카드: GET /projects/{n}/research/sessions 결과 리스트
- 실행 중 안내 (장시간 가능), sonner 토스트
라우팅 & Sidebar:
- App.tsx: /research/:projectId 라우트 추가
- AppShell: 사이드바에 "자율 연구" 메뉴 (Brain 아이콘)
i18n: research.*, nav.research 키 (한/영)
다음 단계: Phase 1.5 — 엔티티/클레임 직접 입력
(백엔드 POST /projects/{n}/entities, /claims 신규 필요)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,29 @@ class ResetProjectRequest(BaseModel):
|
||||
project_name: str | None = None
|
||||
|
||||
|
||||
class ResearchRunByProjectRequest(BaseModel):
|
||||
"""Run autonomous research against an existing DB project (no config_path)."""
|
||||
|
||||
project_name: str
|
||||
source_name: str
|
||||
url: str | None = None
|
||||
seed_entity_id: int | None = None
|
||||
goal: str = "Semantic ontology exploration"
|
||||
extractor_provider: str = "lm_studio"
|
||||
extractor_model: str | None = None
|
||||
extractor_base_url: str | None = "http://localhost:1234/v1"
|
||||
check_robots_txt: bool = False
|
||||
respect_robots_txt: bool | None = None
|
||||
max_depth: int = 2
|
||||
max_steps: int = 12
|
||||
max_branch: int = 8
|
||||
min_relevance: float = 0.35
|
||||
same_domain_only: bool = True
|
||||
analyze_page_types: list[str] = Field(
|
||||
default_factory=lambda: ["ProductPage", "BrandStoryPage", "ReviewPage"]
|
||||
)
|
||||
|
||||
|
||||
class UpdateClaimConfidenceRequest(BaseModel):
|
||||
confidence: float
|
||||
reason: str | None = None
|
||||
@@ -796,6 +819,56 @@ def register_routes(app, database_url: str) -> None:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return asdict(result)
|
||||
|
||||
@app.post("/research/run/by-project")
|
||||
def run_research_by_project(request: ResearchRunByProjectRequest):
|
||||
"""Run research against an existing DB project (no config_path)."""
|
||||
with session_scope(database_url) as session:
|
||||
repo = KnowledgeRepository(session)
|
||||
project = repo.get_project(request.project_name)
|
||||
config_dict = project.config or {}
|
||||
if not config_dict:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Project '{request.project_name}' has no stored config",
|
||||
)
|
||||
config = project_config_from_dict(config_dict)
|
||||
try:
|
||||
source_config = config.source_by_name(request.source_name)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
check_robots = request.respect_robots_txt
|
||||
if check_robots is None:
|
||||
check_robots = request.check_robots_txt
|
||||
source_config.respect_robots_txt = check_robots
|
||||
|
||||
loop = GraphResearchLoop(
|
||||
repo,
|
||||
extractor_for_domain(
|
||||
config.domain,
|
||||
provider=request.extractor_provider,
|
||||
model=request.extractor_model,
|
||||
base_url=request.extractor_base_url,
|
||||
),
|
||||
)
|
||||
try:
|
||||
result = loop.run(
|
||||
project_config=config,
|
||||
source_name=request.source_name,
|
||||
seed_url=request.url or None,
|
||||
seed_entity_id=request.seed_entity_id,
|
||||
goal=request.goal,
|
||||
max_depth=max(request.max_depth, 0),
|
||||
max_steps=max(min(request.max_steps, 50), 1),
|
||||
max_branch=max(min(request.max_branch, 30), 1),
|
||||
min_relevance=min(max(request.min_relevance, 0.0), 1.0),
|
||||
same_domain_only=request.same_domain_only,
|
||||
analyze_page_types=set(request.analyze_page_types),
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return asdict(result)
|
||||
|
||||
@app.get("/projects/{project_name}/research/sessions")
|
||||
def research_sessions(project_name: str, limit: int = 25):
|
||||
with session_scope(database_url) as session:
|
||||
|
||||
Reference in New Issue
Block a user