diff --git a/README.md b/README.md index 14eee4e..071dc22 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,9 @@ When an extractor is present and returns candidates, analysis stores those reviewable candidates; when it returns no candidates, the deterministic heuristic generator remains the fallback. +The FastAPI settings object also accepts `llm_provider` and `llm_model`. By +default `llm_provider` is unset, so analysis is fully offline and deterministic. + ## Agent-Facing Endpoints The v0.1 API covers the main registration, analysis, review, search, and inspection loop: diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py index 95bd3f2..cfbe85d 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -7,6 +7,7 @@ from fastapi import Depends, FastAPI, HTTPException from pydantic import BaseModel, Field from repo_registry.core.service import RegistryService +from repo_registry.llm_extraction import LLMCandidateExtractor, create_llm_connect_adapter from repo_registry.repo_ingestion.git import GitIngestionService from repo_registry.storage.sqlite import NotFoundError, RegistryStore @@ -14,6 +15,8 @@ from repo_registry.storage.sqlite import NotFoundError, RegistryStore class Settings(BaseModel): database_path: str = Field(default="var/repo-registry.sqlite3") checkout_root: str = Field(default="var/checkouts") + llm_provider: str | None = Field(default=None) + llm_model: str | None = Field(default=None) def get_settings() -> Settings: @@ -25,7 +28,18 @@ def get_service(settings: Settings = Depends(get_settings)) -> RegistryService: database_path.parent.mkdir(parents=True, exist_ok=True) store = RegistryStore(database_path) store.initialize() - return RegistryService(store, ingestion=GitIngestionService(settings.checkout_root)) + llm_extractor = None + if settings.llm_provider: + adapter = create_llm_connect_adapter( + settings.llm_provider, + model=settings.llm_model, + ) + llm_extractor = LLMCandidateExtractor(adapter) + return RegistryService( + store, + ingestion=GitIngestionService(settings.checkout_root), + llm_extractor=llm_extractor, + ) class RepositoryCreate(BaseModel): diff --git a/tests/test_web_api.py b/tests/test_web_api.py index a9c092b..dea7197 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -1,6 +1,7 @@ from fastapi.testclient import TestClient -from repo_registry.web_api.app import Settings, app, get_settings +from repo_registry.web_api import app as app_module +from repo_registry.web_api.app import Settings, app, get_service, get_settings def test_api_manual_registry_loop(tmp_path): @@ -164,6 +165,34 @@ def test_api_registers_repository_from_url_metadata(tmp_path): app.dependency_overrides.clear() +def test_api_service_settings_can_enable_llm_extractor(monkeypatch, tmp_path): + class Response: + content = '{"abilities": [{"name": "Configured LLM Ability"}]}' + + class Adapter: + def execute_prompt(self, prompt, config): + return Response() + + calls = [] + + def fake_create_adapter(provider, model=None): + calls.append((provider, model)) + return Adapter() + + monkeypatch.setattr(app_module, "create_llm_connect_adapter", fake_create_adapter) + service = get_service( + Settings( + database_path=str(tmp_path / "llm-settings.sqlite3"), + checkout_root=str(tmp_path / "checkouts"), + llm_provider="mock", + llm_model="demo-model", + ) + ) + + assert calls == [("mock", "demo-model")] + assert service.llm_extractor is not None + + def test_api_analysis_run_loop(tmp_path): source = tmp_path / "repo" source.mkdir()