Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .dockerignore
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
36 changes: 36 additions & 0 deletions Dockerfile
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"]
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,67 @@ Unlike typical AI assistants, this agent:

---

## Run With Docker

This repository has two runtime parts:

- Python backend (FastAPI + tools) -> Dockerized below.
- Browser extension (WXT/React) -> runs on your host browser, not inside Docker.

### 1) Prepare environment

Copy the sample environment file and set your keys:

```bash
cp .env.example .env
```

Minimum required value for backend boot:

- `GOOGLE_API_KEY`

If you plan to use Google OAuth features (Gmail/Calendar login), also set:

- `GOOGLE_CLIENT_SECRET`

### 2) Build and run API container

```bash
docker compose up --build -d agentic-api
```

Health check:

```bash
curl http://localhost:5454/api/genai/health/
```

Stop container:

```bash
docker compose down
```

### 3) Optional: run MCP mode in Docker

```bash
docker compose --profile mcp up --build agentic-mcp
```

### 4) Run extension locally against Docker backend

From `extension/`:

```bash
pnpm install
echo "VITE_API_URL=http://localhost:5454" > .env
pnpm dev
```

Then load the unpacked extension from `extension/.output/chrome-mv3`.

---

## Architecture

<img width="1973" height="1305" alt="Agentic Browser Architecture" src="https://github.com/user-attachments/assets/21dac6a5-c9d7-499a-8648-becdb4a04bba" />
Expand Down
49 changes: 45 additions & 4 deletions core/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Copy link
Copy Markdown
Owner

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

  • allow it to be setup via ui - priority 1
  • allow then by get settings - pydantic settings
  • then .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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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."

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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

Expand Down Expand Up @@ -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",
Expand All @@ -204,12 +211,18 @@ def summarize_text(self, text: str) -> str:
"openrouter": "openrouter_api_key",
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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
beacuse most llm apis areopenai compliant

# 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()
Expand All @@ -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,
)


Expand All @@ -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


Expand All @@ -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:
Expand Down
Loading