-
Notifications
You must be signed in to change notification settings - Fork 5
Implement LLM-based entity extractor #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4719d7e
71c765c
83daf03
d3d7ce3
3c5b780
c017091
e9f7dbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Git | ||
| .git | ||
| .gitignore | ||
|
|
||
| # Python cache/env | ||
| __pycache__/ | ||
| *.py[codz] | ||
| *.pyo | ||
| *.pyd | ||
| .pytest_cache/ | ||
| .mypy_cache/ | ||
| .ruff_cache/ | ||
| .venv/ | ||
| venv/ | ||
| env/ | ||
|
|
||
| # Local env/secrets | ||
| .env | ||
| .env.* | ||
| !.env.example | ||
|
|
||
| # Build artifacts | ||
| build/ | ||
| dist/ | ||
| *.egg-info/ | ||
|
|
||
| # Runtime/local data | ||
| uploads/ | ||
|
|
||
| # Frontend workspace is not needed for backend container runtime | ||
| extension/node_modules/ | ||
| extension/.wxt/ | ||
| extension/.output/ | ||
| extension/.env | ||
|
|
||
| # Editor/system files | ||
| .DS_Store | ||
| Thumbs.db |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # syntax=docker/dockerfile:1.7 | ||
|
|
||
| FROM python:3.12-slim AS runtime | ||
|
|
||
| ENV PYTHONDONTWRITEBYTECODE=1 \ | ||
| PYTHONUNBUFFERED=1 \ | ||
| UV_COMPILE_BYTECODE=1 \ | ||
| UV_LINK_MODE=copy | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Runtime deps: | ||
| # - ffmpeg is required by faster-whisper for voice transcription routes | ||
| # - git is used by repo-related tooling | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| ffmpeg \ | ||
| git \ | ||
| ca-certificates \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Install uv binary for reproducible lockfile-based installs. | ||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv | ||
|
|
||
| # Install Python dependencies first for better layer caching. | ||
| COPY pyproject.toml uv.lock ./ | ||
| RUN uv sync --frozen --no-dev --no-install-project | ||
|
|
||
| # Copy application code. | ||
| COPY . . | ||
|
|
||
| EXPOSE 5454 | ||
|
|
||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ | ||
| CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5454/api/genai/health/', timeout=3)" || exit 1 | ||
|
|
||
| CMD ["uv", "run", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "5454"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,12 +131,18 @@ def __init__( | |
| elif "base_url_override" in config: | ||
| final_base_url = config["base_url_override"] | ||
| elif config.get("base_url_env"): | ||
| final_base_url = os.getenv(config["base_url_env"]) | ||
| # Try env var first, then fall back to the pydantic-settings default | ||
| # (which already defaults ollama to http://localhost:11434) | ||
| final_base_url = os.getenv(config["base_url_env"]) or getattr( | ||
| get_settings(), config["base_url_env"].lower(), None | ||
| ) | ||
|
|
||
| if final_base_url: | ||
| base_url_param_name = config["param_map"].get("base_url", "base_url") | ||
| params[base_url_param_name] = final_base_url | ||
| elif config.get("base_url_env") and not final_base_url: | ||
| # For providers that *need* a base URL, raise; but for ollama we | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we shd suppoert vllm also not just ollama |
||
| # have a sensible default in Settings so we should never reach here. | ||
| raise ValueError( | ||
| f"Base URL for '{self.provider}' not found. " | ||
| f"Please provide it directly or set the '{config['base_url_env']}' environment variable." | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. support mlx model as well rn this only supports llama cpp models which are very slow on macs we need mlx for macos local model testing |
||
|
|
@@ -196,6 +202,7 @@ def summarize_text(self, text: str) -> str: | |
| _default: LargeLanguageModel | None = None | ||
|
|
||
|
|
||
| # Maps provider → secret name for API keys (cloud providers only) | ||
| _PROVIDER_TO_SECRET = { | ||
| "google": "google_api_key", | ||
| "openai": "openai_api_key", | ||
|
|
@@ -204,12 +211,18 @@ def summarize_text(self, text: str) -> str: | |
| "openrouter": "openrouter_api_key", | ||
| } | ||
|
|
||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. modify the config structure for cloud llms also have an option for these base url especially for openai |
||
| # Maps provider → secret name for base URLs (local/self-hosted providers) | ||
| _PROVIDER_TO_BASE_URL_SECRET = { | ||
| "ollama": "ollama_base_url", | ||
| } | ||
|
|
||
|
|
||
| def _build_default( | ||
| provider: str | None = None, | ||
| model: str | None = None, | ||
| temperature: float | None = None, | ||
| api_key: str | None = None, | ||
| base_url: str | None = None, | ||
| ) -> LargeLanguageModel: | ||
| s = get_settings() | ||
| p = (provider or s.default_llm_provider or "google").lower() | ||
|
|
@@ -227,11 +240,21 @@ def _build_default( | |
| api_key = get_secrets_service().resolve_sync(secret_name) if secret_name else "" | ||
| except Exception: | ||
| api_key = getattr(get_settings(), secret_name, "") if secret_name else "" | ||
| # Resolve base URL for providers that need it (e.g. Ollama) — sync path. | ||
| if base_url is None: | ||
| base_url_secret = _PROVIDER_TO_BASE_URL_SECRET.get(p) | ||
| if base_url_secret: | ||
| try: | ||
| from services.secrets_service import get_secrets_service | ||
| base_url = get_secrets_service().resolve_sync(base_url_secret) or None | ||
| except Exception: | ||
| base_url = None | ||
| return LargeLanguageModel( | ||
| model_name=model or cfg.get("default_model"), | ||
| api_key=api_key or "", | ||
| provider=p, # type: ignore[arg-type] | ||
| temperature=temperature if temperature is not None else 0.4, | ||
| base_url=base_url, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -252,27 +275,42 @@ async def reload_default_llm() -> LargeLanguageModel: | |
| except Exception: | ||
| override = None | ||
| provider = (override or {}).get("provider") | ||
| p_lower = (provider or "google").lower() | ||
|
|
||
| # Resolve API key for cloud providers | ||
| api_key: str | None = None | ||
| secret_name = _PROVIDER_TO_SECRET.get((provider or "google").lower()) | ||
| secret_name = _PROVIDER_TO_SECRET.get(p_lower) | ||
| if secret_name: | ||
| try: | ||
| from services.secrets_service import get_secrets_service | ||
| api_key = await get_secrets_service().resolve(secret_name) | ||
| except Exception: | ||
| api_key = None | ||
|
|
||
| # Resolve base URL for self-hosted providers (e.g. Ollama) | ||
| base_url: str | None = None | ||
| base_url_secret = _PROVIDER_TO_BASE_URL_SECRET.get(p_lower) | ||
| if base_url_secret: | ||
| try: | ||
| from services.secrets_service import get_secrets_service | ||
| base_url = await get_secrets_service().resolve(base_url_secret) or None | ||
| except Exception: | ||
| base_url = None | ||
|
|
||
| if override: | ||
| try: | ||
| _default = _build_default( | ||
| provider=provider, | ||
| model=override.get("model"), | ||
| temperature=override.get("temperature"), | ||
| api_key=api_key, | ||
| base_url=base_url, | ||
| ) | ||
| except Exception as exc: | ||
| print(f"Failed to apply LLM override {override!r}, falling back to env defaults: {exc}") | ||
| _default = _build_default(api_key=api_key) | ||
| else: | ||
| _default = _build_default(api_key=api_key) | ||
| _default = _build_default(api_key=api_key, base_url=base_url) | ||
| return _default | ||
|
|
||
|
|
||
|
|
@@ -289,7 +327,10 @@ def __getattr__(self, item): | |
| return getattr(self._target(), item) | ||
|
|
||
| def __call__(self, *args, **kwargs): | ||
| return self._target()(*args, **kwargs) | ||
| # LangChain chat models (e.g. ChatOllama) no longer support the | ||
| # deprecated callable syntax `model(messages)`. Use .invoke() instead, | ||
| # which is the standard LangChain v0.2+ API for all providers. | ||
| return self._target().invoke(*args, **kwargs) | ||
|
|
||
| def __repr__(self): | ||
| try: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make the base url configurale for all