localgate 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- localgate-0.7.0/.env.example +62 -0
- localgate-0.7.0/.github/CODEOWNERS +11 -0
- localgate-0.7.0/.github/FUNDING.yml +1 -0
- localgate-0.7.0/.github/ISSUE_TEMPLATE/bug_report.md +13 -0
- localgate-0.7.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
- localgate-0.7.0/.github/ISSUE_TEMPLATE/feature_request.md +9 -0
- localgate-0.7.0/.github/PULL_REQUEST_TEMPLATE.md +7 -0
- localgate-0.7.0/.github/workflows/ci.yml +110 -0
- localgate-0.7.0/.github/workflows/docker.yml +45 -0
- localgate-0.7.0/.github/workflows/release.yml +50 -0
- localgate-0.7.0/.gitignore +44 -0
- localgate-0.7.0/.pre-commit-config.yaml +29 -0
- localgate-0.7.0/.python-version +1 -0
- localgate-0.7.0/CHANGELOG.md +108 -0
- localgate-0.7.0/CODE_OF_CONDUCT.md +6 -0
- localgate-0.7.0/CONTRIBUTING.md +115 -0
- localgate-0.7.0/Dockerfile +55 -0
- localgate-0.7.0/LICENSE +21 -0
- localgate-0.7.0/Makefile +37 -0
- localgate-0.7.0/PKG-INFO +239 -0
- localgate-0.7.0/README.md +188 -0
- localgate-0.7.0/SECURITY.md +86 -0
- localgate-0.7.0/docker-compose.yml +26 -0
- localgate-0.7.0/docs/api-reference.md +254 -0
- localgate-0.7.0/docs/architecture.md +172 -0
- localgate-0.7.0/docs/configuration.md +158 -0
- localgate-0.7.0/docs/database-setup.md +154 -0
- localgate-0.7.0/docs/decisions/0001-dependencies-over-middleware.md +50 -0
- localgate-0.7.0/docs/decisions/0002-json-embeddings-over-pgvector.md +61 -0
- localgate-0.7.0/docs/decisions/0003-sha256-for-api-keys.md +60 -0
- localgate-0.7.0/docs/deployment.md +174 -0
- localgate-0.7.0/docs/getting-started.md +169 -0
- localgate-0.7.0/docs/images/dashboard-keys.png +0 -0
- localgate-0.7.0/docs/images/dashboard-overview.png +0 -0
- localgate-0.7.0/docs/rag-memory.md +153 -0
- localgate-0.7.0/examples/curl_examples.sh +61 -0
- localgate-0.7.0/examples/docker-compose.postgres.yml +44 -0
- localgate-0.7.0/examples/langchain_example.py +5 -0
- localgate-0.7.0/examples/openai_sdk_example.py +71 -0
- localgate-0.7.0/pyproject.toml +122 -0
- localgate-0.7.0/scripts/benchmark.py +129 -0
- localgate-0.7.0/scripts/dev-setup.sh +11 -0
- localgate-0.7.0/src/localgate/__init__.py +8 -0
- localgate-0.7.0/src/localgate/__main__.py +6 -0
- localgate-0.7.0/src/localgate/agent/__init__.py +8 -0
- localgate-0.7.0/src/localgate/agent/gitutil.py +104 -0
- localgate-0.7.0/src/localgate/agent/ignore.py +66 -0
- localgate-0.7.0/src/localgate/agent/loop.py +322 -0
- localgate-0.7.0/src/localgate/agent/memory.py +149 -0
- localgate-0.7.0/src/localgate/agent/render.py +64 -0
- localgate-0.7.0/src/localgate/agent/repl.py +238 -0
- localgate-0.7.0/src/localgate/agent/tools.py +292 -0
- localgate-0.7.0/src/localgate/api/__init__.py +0 -0
- localgate-0.7.0/src/localgate/api/chat.py +410 -0
- localgate-0.7.0/src/localgate/api/completions.py +82 -0
- localgate-0.7.0/src/localgate/api/config.py +150 -0
- localgate-0.7.0/src/localgate/api/conversations.py +69 -0
- localgate-0.7.0/src/localgate/api/deps.py +85 -0
- localgate-0.7.0/src/localgate/api/embeddings.py +55 -0
- localgate-0.7.0/src/localgate/api/export.py +92 -0
- localgate-0.7.0/src/localgate/api/health.py +126 -0
- localgate-0.7.0/src/localgate/api/keys.py +114 -0
- localgate-0.7.0/src/localgate/api/models.py +39 -0
- localgate-0.7.0/src/localgate/api/usage.py +31 -0
- localgate-0.7.0/src/localgate/app.py +183 -0
- localgate-0.7.0/src/localgate/backends/__init__.py +98 -0
- localgate-0.7.0/src/localgate/backends/base.py +43 -0
- localgate-0.7.0/src/localgate/backends/fake.py +90 -0
- localgate-0.7.0/src/localgate/backends/llamacpp.py +16 -0
- localgate-0.7.0/src/localgate/backends/ollama.py +23 -0
- localgate-0.7.0/src/localgate/backends/openai_compat.py +97 -0
- localgate-0.7.0/src/localgate/backends/vllm.py +16 -0
- localgate-0.7.0/src/localgate/cli.py +471 -0
- localgate-0.7.0/src/localgate/config.py +142 -0
- localgate-0.7.0/src/localgate/core/__init__.py +0 -0
- localgate-0.7.0/src/localgate/core/auth.py +46 -0
- localgate-0.7.0/src/localgate/core/cache.py +91 -0
- localgate-0.7.0/src/localgate/core/db_config_store.py +46 -0
- localgate-0.7.0/src/localgate/core/errors.py +146 -0
- localgate-0.7.0/src/localgate/core/logging.py +66 -0
- localgate-0.7.0/src/localgate/core/metrics.py +68 -0
- localgate-0.7.0/src/localgate/core/rate_limiter.py +66 -0
- localgate-0.7.0/src/localgate/core/streaming.py +41 -0
- localgate-0.7.0/src/localgate/core/token_counter.py +58 -0
- localgate-0.7.0/src/localgate/core/types.py +159 -0
- localgate-0.7.0/src/localgate/dashboard/__init__.py +0 -0
- localgate-0.7.0/src/localgate/dashboard/routes.py +29 -0
- localgate-0.7.0/src/localgate/dashboard/static/index.html +878 -0
- localgate-0.7.0/src/localgate/db/__init__.py +0 -0
- localgate-0.7.0/src/localgate/db/engine.py +134 -0
- localgate-0.7.0/src/localgate/db/migrations/__init__.py +1 -0
- localgate-0.7.0/src/localgate/db/migrations/alembic.ini +46 -0
- localgate-0.7.0/src/localgate/db/migrations/env.py +92 -0
- localgate-0.7.0/src/localgate/db/migrations/script.py.mako +27 -0
- localgate-0.7.0/src/localgate/db/migrations/versions/0001_initial_schema.py +92 -0
- localgate-0.7.0/src/localgate/db/migrations/versions/0002_summaries_and_accounting.py +135 -0
- localgate-0.7.0/src/localgate/db/models.py +133 -0
- localgate-0.7.0/src/localgate/db/repositories/__init__.py +0 -0
- localgate-0.7.0/src/localgate/db/repositories/conversations.py +114 -0
- localgate-0.7.0/src/localgate/db/repositories/embeddings.py +103 -0
- localgate-0.7.0/src/localgate/db/repositories/keys.py +82 -0
- localgate-0.7.0/src/localgate/db/repositories/usage.py +146 -0
- localgate-0.7.0/src/localgate/memory/__init__.py +0 -0
- localgate-0.7.0/src/localgate/memory/chunker.py +23 -0
- localgate-0.7.0/src/localgate/memory/context_builder.py +53 -0
- localgate-0.7.0/src/localgate/memory/embedder.py +7 -0
- localgate-0.7.0/src/localgate/memory/retriever.py +31 -0
- localgate-0.7.0/src/localgate/memory/summarizer.py +125 -0
- localgate-0.7.0/src/localgate/middleware/__init__.py +10 -0
- localgate-0.7.0/src/localgate/middleware/logging_middleware.py +113 -0
- localgate-0.7.0/tests/__init__.py +0 -0
- localgate-0.7.0/tests/conftest.py +80 -0
- localgate-0.7.0/tests/e2e/__init__.py +0 -0
- localgate-0.7.0/tests/e2e/test_full_flow.py +71 -0
- localgate-0.7.0/tests/integration/__init__.py +0 -0
- localgate-0.7.0/tests/integration/test_chat_endpoint.py +143 -0
- localgate-0.7.0/tests/integration/test_chat_resilience.py +206 -0
- localgate-0.7.0/tests/integration/test_cli.py +146 -0
- localgate-0.7.0/tests/integration/test_database_connectors.py +112 -0
- localgate-0.7.0/tests/integration/test_key_management.py +152 -0
- localgate-0.7.0/tests/integration/test_migrations.py +247 -0
- localgate-0.7.0/tests/integration/test_operations.py +389 -0
- localgate-0.7.0/tests/integration/test_rag_pipeline.py +137 -0
- localgate-0.7.0/tests/integration/test_summarizer.py +100 -0
- localgate-0.7.0/tests/unit/__init__.py +0 -0
- localgate-0.7.0/tests/unit/test_agent_gitutil.py +88 -0
- localgate-0.7.0/tests/unit/test_agent_ignore.py +67 -0
- localgate-0.7.0/tests/unit/test_agent_loop.py +361 -0
- localgate-0.7.0/tests/unit/test_agent_memory.py +186 -0
- localgate-0.7.0/tests/unit/test_agent_render.py +42 -0
- localgate-0.7.0/tests/unit/test_agent_repl.py +259 -0
- localgate-0.7.0/tests/unit/test_agent_streaming.py +107 -0
- localgate-0.7.0/tests/unit/test_agent_tools.py +205 -0
- localgate-0.7.0/tests/unit/test_auth.py +26 -0
- localgate-0.7.0/tests/unit/test_backend_error_messages.py +53 -0
- localgate-0.7.0/tests/unit/test_backends.py +207 -0
- localgate-0.7.0/tests/unit/test_cache.py +82 -0
- localgate-0.7.0/tests/unit/test_chunker.py +35 -0
- localgate-0.7.0/tests/unit/test_config.py +84 -0
- localgate-0.7.0/tests/unit/test_rate_limiter.py +68 -0
- localgate-0.7.0/tests/unit/test_token_counter.py +28 -0
- localgate-0.7.0/uv.lock +2093 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copy to .env and edit. Every setting is documented in docs/configuration.md.
|
|
2
|
+
|
|
3
|
+
# --- Deployment ------------------------------------------------------------
|
|
4
|
+
# `production` makes the gateway REFUSE to start with the placeholder admin key below.
|
|
5
|
+
LOCALGATE_ENVIRONMENT=development
|
|
6
|
+
|
|
7
|
+
# --- Auth ------------------------------------------------------------------
|
|
8
|
+
# Generate a real one: openssl rand -hex 32
|
|
9
|
+
LOCALGATE_ADMIN_KEY=change-me-in-production
|
|
10
|
+
LOCALGATE_DEFAULT_RATE_LIMIT_PER_MIN=60
|
|
11
|
+
|
|
12
|
+
# --- Server ----------------------------------------------------------------
|
|
13
|
+
LOCALGATE_HOST=0.0.0.0
|
|
14
|
+
LOCALGATE_PORT=8000
|
|
15
|
+
# Only needed if a browser calls the API directly. Comma-separated.
|
|
16
|
+
# LOCALGATE_CORS_ORIGINS=http://localhost:3000
|
|
17
|
+
|
|
18
|
+
# --- Inference backend -----------------------------------------------------
|
|
19
|
+
# ollama | vllm | llamacpp | openai_compat (or any installed plugin)
|
|
20
|
+
LOCALGATE_BACKEND_TYPE=ollama
|
|
21
|
+
LOCALGATE_BACKEND_URL=http://localhost:11434
|
|
22
|
+
LOCALGATE_DEFAULT_MODEL=llama3
|
|
23
|
+
# Raise for large models on CPU.
|
|
24
|
+
LOCALGATE_BACKEND_TIMEOUT=120
|
|
25
|
+
# For upstreams that need their own auth.
|
|
26
|
+
# LOCALGATE_BACKEND_API_KEY=
|
|
27
|
+
|
|
28
|
+
# Map friendly names to real model ids, so you can swap models without touching clients.
|
|
29
|
+
# LOCALGATE_MODEL_ALIASES={"fast":"phi4-mini","smart":"llama3:70b"}
|
|
30
|
+
|
|
31
|
+
# --- Database --------------------------------------------------------------
|
|
32
|
+
# The driver must be ASYNC. Note `+asyncpg`, not a bare `postgresql://`.
|
|
33
|
+
LOCALGATE_DATABASE_URL=sqlite+aiosqlite:///./localgate.db
|
|
34
|
+
# Postgres: LOCALGATE_DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/localgate
|
|
35
|
+
# Neon: LOCALGATE_DATABASE_URL=postgresql+asyncpg://user:pass@ep-xxx.neon.tech/localgate?ssl=require
|
|
36
|
+
# (Neon's copy-paste string says `sslmode=require`; asyncpg spells it `ssl=require`.)
|
|
37
|
+
|
|
38
|
+
# --- Memory / RAG ----------------------------------------------------------
|
|
39
|
+
LOCALGATE_MEMORY_ENABLED=true
|
|
40
|
+
# Must be pulled on the backend: ollama pull nomic-embed-text
|
|
41
|
+
LOCALGATE_EMBEDDING_MODEL=nomic-embed-text
|
|
42
|
+
LOCALGATE_CHUNK_SIZE=512
|
|
43
|
+
LOCALGATE_CHUNK_OVERLAP=50
|
|
44
|
+
LOCALGATE_MAX_RETRIEVED_CHUNKS=5
|
|
45
|
+
# Cosine-similarity floor for injecting a chunk. 0.0 = no floor. With a real embedding
|
|
46
|
+
# model, 0.3-0.5 is typical — see docs/rag-memory.md for how to tune it from your logs.
|
|
47
|
+
LOCALGATE_MEMORY_MIN_SCORE=0.0
|
|
48
|
+
# Summarize older turns past this many messages. 0 disables.
|
|
49
|
+
LOCALGATE_SUMMARIZE_AFTER_MESSAGES=20
|
|
50
|
+
|
|
51
|
+
# --- Prompt cache ----------------------------------------------------------
|
|
52
|
+
# Off by default: caching makes identical requests return identical completions, which is
|
|
53
|
+
# a real behaviour change if you sample at temperature > 0.
|
|
54
|
+
LOCALGATE_CACHE_ENABLED=false
|
|
55
|
+
LOCALGATE_CACHE_TTL_SECONDS=300
|
|
56
|
+
LOCALGATE_CACHE_MAX_ENTRIES=512
|
|
57
|
+
|
|
58
|
+
# --- Observability ---------------------------------------------------------
|
|
59
|
+
LOCALGATE_LOG_LEVEL=INFO
|
|
60
|
+
# `console` for humans, `json` for log ingestion.
|
|
61
|
+
LOCALGATE_LOG_FORMAT=console
|
|
62
|
+
LOCALGATE_METRICS_ENABLED=true
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Default reviewer for everything.
|
|
2
|
+
* @AnjalLLL
|
|
3
|
+
|
|
4
|
+
# Changes here alter the security posture — auth, key handling, the rules that decide who
|
|
5
|
+
# can call what. They always want a second pair of eyes, even on a one-line diff.
|
|
6
|
+
/src/localgate/core/auth.py @AnjalLLL
|
|
7
|
+
/src/localgate/api/deps.py @AnjalLLL
|
|
8
|
+
/SECURITY.md @AnjalLLL
|
|
9
|
+
|
|
10
|
+
# Migrations are the one thing a user cannot roll back by reverting a commit.
|
|
11
|
+
/src/localgate/db/migrations/ @AnjalLLL
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# github: [AnjalLLL]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: true
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v5
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
30
|
+
run: uv python install ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Sync dependencies
|
|
33
|
+
run: uv sync --all-extras --python ${{ matrix.python-version }}
|
|
34
|
+
|
|
35
|
+
- name: Lint
|
|
36
|
+
run: uv run ruff check src/ tests/
|
|
37
|
+
|
|
38
|
+
- name: Format check
|
|
39
|
+
run: uv run ruff format --check src/ tests/
|
|
40
|
+
|
|
41
|
+
- name: Type check
|
|
42
|
+
run: uv run mypy src/
|
|
43
|
+
|
|
44
|
+
- name: Test
|
|
45
|
+
run: uv run pytest -q --cov --cov-report=term-missing
|
|
46
|
+
|
|
47
|
+
postgres:
|
|
48
|
+
# The SQLite suite proves the logic. This proves the migrations and queries actually
|
|
49
|
+
# run on Postgres, which is what production uses — skipping it is how you ship a
|
|
50
|
+
# migration that only works on SQLite.
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
env:
|
|
53
|
+
LOCALGATE_DATABASE_URL: postgresql+asyncpg://localgate:localgate@localhost:5432/localgate
|
|
54
|
+
LOCALGATE_BACKEND_TYPE: fake
|
|
55
|
+
LOCALGATE_ADMIN_KEY: ci-admin-key
|
|
56
|
+
|
|
57
|
+
services:
|
|
58
|
+
postgres:
|
|
59
|
+
image: postgres:16
|
|
60
|
+
env:
|
|
61
|
+
POSTGRES_USER: localgate
|
|
62
|
+
POSTGRES_PASSWORD: localgate
|
|
63
|
+
POSTGRES_DB: localgate
|
|
64
|
+
ports: ["5432:5432"]
|
|
65
|
+
options: >-
|
|
66
|
+
--health-cmd pg_isready
|
|
67
|
+
--health-interval 10s
|
|
68
|
+
--health-timeout 5s
|
|
69
|
+
--health-retries 5
|
|
70
|
+
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
- uses: astral-sh/setup-uv@v5
|
|
74
|
+
with:
|
|
75
|
+
enable-cache: true
|
|
76
|
+
- run: uv sync --all-extras
|
|
77
|
+
|
|
78
|
+
- name: Migrate a real Postgres database
|
|
79
|
+
run: |
|
|
80
|
+
uv run localgate db upgrade
|
|
81
|
+
uv run localgate db current
|
|
82
|
+
|
|
83
|
+
- name: Migrations are idempotent (startup re-runs them on every boot)
|
|
84
|
+
run: uv run localgate db upgrade
|
|
85
|
+
|
|
86
|
+
- name: Create and list a key against Postgres
|
|
87
|
+
run: |
|
|
88
|
+
uv run localgate keys create --name ci-key
|
|
89
|
+
uv run localgate keys list
|
|
90
|
+
|
|
91
|
+
docker:
|
|
92
|
+
runs-on: ubuntu-latest
|
|
93
|
+
steps:
|
|
94
|
+
- uses: actions/checkout@v4
|
|
95
|
+
|
|
96
|
+
- name: Build the image
|
|
97
|
+
run: docker build -t localgate:ci .
|
|
98
|
+
|
|
99
|
+
- name: The image starts and serves
|
|
100
|
+
run: |
|
|
101
|
+
docker run -d --name lg -p 8000:8000 \
|
|
102
|
+
-e LOCALGATE_BACKEND_TYPE=fake \
|
|
103
|
+
-e LOCALGATE_ADMIN_KEY=ci-admin-key \
|
|
104
|
+
localgate:ci
|
|
105
|
+
for _ in $(seq 1 30); do
|
|
106
|
+
curl -sf http://localhost:8000/health/live > /dev/null && break
|
|
107
|
+
sleep 1
|
|
108
|
+
done
|
|
109
|
+
curl -sf http://localhost:8000/health/live || (docker logs lg && exit 1)
|
|
110
|
+
docker rm -f lg
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Docker
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
docker:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
packages: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: docker/setup-qemu-action@v3
|
|
18
|
+
- uses: docker/setup-buildx-action@v3
|
|
19
|
+
|
|
20
|
+
- uses: docker/login-action@v3
|
|
21
|
+
with:
|
|
22
|
+
registry: ghcr.io
|
|
23
|
+
username: ${{ github.actor }}
|
|
24
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
25
|
+
|
|
26
|
+
- name: Tags and labels
|
|
27
|
+
id: meta
|
|
28
|
+
uses: docker/metadata-action@v5
|
|
29
|
+
with:
|
|
30
|
+
images: ghcr.io/${{ github.repository }}
|
|
31
|
+
tags: |
|
|
32
|
+
type=semver,pattern={{version}}
|
|
33
|
+
type=semver,pattern={{major}}.{{minor}}
|
|
34
|
+
type=raw,value=latest
|
|
35
|
+
|
|
36
|
+
- uses: docker/build-push-action@v6
|
|
37
|
+
with:
|
|
38
|
+
push: true
|
|
39
|
+
# Local models run on Apple Silicon and on Raspberry Pis as often as on x86, so
|
|
40
|
+
# a gateway that only ships amd64 is useless to a large share of its users.
|
|
41
|
+
platforms: linux/amd64,linux/arm64
|
|
42
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
43
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
44
|
+
cache-from: type=gha
|
|
45
|
+
cache-to: type=gha,mode=max
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
# Never publish something that doesn't pass. A tag is not a promise that CI ran — the
|
|
9
|
+
# tag can be pushed from a branch that was never tested, and PyPI releases cannot be
|
|
10
|
+
# unpublished, only yanked.
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: true
|
|
18
|
+
- run: uv sync --all-extras
|
|
19
|
+
- run: uv run ruff check src/ tests/
|
|
20
|
+
- run: uv run mypy src/
|
|
21
|
+
- run: uv run pytest -q
|
|
22
|
+
|
|
23
|
+
publish:
|
|
24
|
+
needs: test
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment: release
|
|
27
|
+
permissions:
|
|
28
|
+
# Trusted publishing: no API token to store, rotate, or leak.
|
|
29
|
+
id-token: write
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: astral-sh/setup-uv@v5
|
|
34
|
+
|
|
35
|
+
# __version__ and pyproject's version are two places that can disagree, and the one
|
|
36
|
+
# users see at runtime is __version__. A release whose version lies about itself is
|
|
37
|
+
# worse than no release, so the tag must match both.
|
|
38
|
+
- name: Check the tag matches the declared version
|
|
39
|
+
run: |
|
|
40
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
41
|
+
INIT=$(sed -n 's/^__version__ = "\(.*\)"/\1/p' src/localgate/__init__.py)
|
|
42
|
+
TOML=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml | head -1)
|
|
43
|
+
echo "tag=$TAG __init__=$INIT pyproject=$TOML"
|
|
44
|
+
test "$TAG" = "$INIT" && test "$TAG" = "$TOML"
|
|
45
|
+
|
|
46
|
+
- name: Build
|
|
47
|
+
run: uv build
|
|
48
|
+
|
|
49
|
+
- name: Publish
|
|
50
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
|
|
8
|
+
# Environments
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Secrets and local state. .env holds the admin key; localgate.config.json holds the
|
|
14
|
+
# established database URL, credentials included.
|
|
15
|
+
.env
|
|
16
|
+
!.env.example
|
|
17
|
+
localgate.config.json
|
|
18
|
+
|
|
19
|
+
# Private planning notes — kept locally, never published.
|
|
20
|
+
ROADMAP.md
|
|
21
|
+
|
|
22
|
+
# Local codegen / patch scratch scripts
|
|
23
|
+
apply_*.sh
|
|
24
|
+
reapply_*.sh
|
|
25
|
+
|
|
26
|
+
# Databases
|
|
27
|
+
*.db
|
|
28
|
+
*.sqlite
|
|
29
|
+
*.sqlite3
|
|
30
|
+
|
|
31
|
+
# Tooling caches
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
.mypy_cache/
|
|
34
|
+
.ruff_cache/
|
|
35
|
+
.coverage
|
|
36
|
+
coverage.xml
|
|
37
|
+
htmlcov/
|
|
38
|
+
|
|
39
|
+
# Editors / OS
|
|
40
|
+
.vscode/
|
|
41
|
+
.idea/
|
|
42
|
+
*.swp
|
|
43
|
+
.DS_Store
|
|
44
|
+
Thumbs.db
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.9.6
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
10
|
+
rev: v5.0.0
|
|
11
|
+
hooks:
|
|
12
|
+
- id: trailing-whitespace
|
|
13
|
+
- id: end-of-file-fixer
|
|
14
|
+
- id: check-yaml
|
|
15
|
+
- id: check-toml
|
|
16
|
+
- id: check-merge-conflict
|
|
17
|
+
- id: check-added-large-files
|
|
18
|
+
# .gitignore already excludes .env, but `git add -f` exists and a committed .env
|
|
19
|
+
# leaks the admin key. This is the second line of defence.
|
|
20
|
+
- id: detect-private-key
|
|
21
|
+
|
|
22
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
23
|
+
rev: v1.14.1
|
|
24
|
+
hooks:
|
|
25
|
+
- id: mypy
|
|
26
|
+
files: ^src/
|
|
27
|
+
additional_dependencies:
|
|
28
|
+
- pydantic>=2.7
|
|
29
|
+
- pydantic-settings>=2.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.7.0] — 2026-07-19
|
|
8
|
+
|
|
9
|
+
A coding agent, backed by whatever model localgate is already pointed at.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`localgate code`** — reads and edits files in a project directory, backed directly by
|
|
14
|
+
the configured inference backend (no API key needed, same pattern as `localgate health`).
|
|
15
|
+
Run it with no argument for an interactive REPL (`/exit`, `/clear`, `/model`, `/undo`), or
|
|
16
|
+
`localgate code "task"` for a single-shot run.
|
|
17
|
+
- Tools: `read_file`, `write_file`, `list_directory`, `search_files` (grep-like), `git_status`,
|
|
18
|
+
`git_diff`. All confined to the project root; `.gitignore` and `.localgateignore` keep
|
|
19
|
+
secrets and generated directories out of the model's reach. No shell/`run_command` tool —
|
|
20
|
+
deliberately.
|
|
21
|
+
- A git-aware safety net: a one-time warning before writing into a dirty tree (`--force` to
|
|
22
|
+
skip), optional `--auto-commit` (tagged `localgate-agent:`), and `/undo` to revert the last
|
|
23
|
+
write or the last agent commit.
|
|
24
|
+
- Colored diffs before every write, streamed responses, and a spinner while waiting on the
|
|
25
|
+
model — via `rich`.
|
|
26
|
+
- Session memory: conversation history and recalled context persist per project
|
|
27
|
+
(`.localgate/session_id`), reusing the same RAG memory tables the HTTP API uses.
|
|
28
|
+
`--no-memory` to opt out for a single run.
|
|
29
|
+
- A fallback parser for models that print a tool call as plain-text JSON instead of using
|
|
30
|
+
structured `tool_calls` (observed with `qwen2.5-coder` via Ollama) — same execution path as
|
|
31
|
+
a real tool call, so confirmation/diff/memory logic isn't duplicated.
|
|
32
|
+
- Shell completion: `localgate --install-completion`.
|
|
33
|
+
|
|
34
|
+
## [0.6.0] — 2026-07-14
|
|
35
|
+
|
|
36
|
+
The release where the advertised features actually work.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **The vLLM, llama.cpp and generic OpenAI backends did not work at all.** They declared
|
|
41
|
+
`InferenceBackend` subclasses that implemented none of its abstract methods, so
|
|
42
|
+
`get_backend("vllm")` raised `TypeError` at startup. Three of the four advertised backends
|
|
43
|
+
were unusable. They now share a real OpenAI-compatible HTTP implementation.
|
|
44
|
+
- **Malformed chat requests returned 500 instead of 422.** The handler read `body["messages"]`
|
|
45
|
+
off a raw dict, so a missing field was a crash rather than a client error. Every request
|
|
46
|
+
body is now validated by Pydantic.
|
|
47
|
+
- **The admin key was compared with `==`**, which short-circuits at the first differing byte
|
|
48
|
+
and leaks the secret's prefix length through timing. Now `hmac.compare_digest`.
|
|
49
|
+
- **The rate limiter never forgot a key it had seen**, leaking one dict entry per key
|
|
50
|
+
forever. Expired windows are now swept.
|
|
51
|
+
- **Errors did not use the OpenAI envelope**, so the OpenAI SDK could not parse them. All
|
|
52
|
+
errors — including validation failures and mid-stream backend failures — now return
|
|
53
|
+
`{"error": {"message", "type", "code"}}`.
|
|
54
|
+
- **A mid-stream backend failure left clients hanging.** It now reports the error inside the
|
|
55
|
+
stream and still terminates with `[DONE]`.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **Migrations.** The schema is versioned with Alembic; `create_all` is gone. Databases from
|
|
60
|
+
0.1–0.2 (tables, but no `alembic_version`) are detected and adopted automatically without
|
|
61
|
+
data loss.
|
|
62
|
+
- **A real CLI.** `keys create/list/revoke/usage`, `db upgrade/current/init`, `health`,
|
|
63
|
+
`backends` — the commands the README had been advertising all along.
|
|
64
|
+
- **`/v1/models`, `/v1/embeddings`, `/v1/completions`** — previously empty files. The legacy
|
|
65
|
+
completions route is implemented by translating to chat, so it works against backends that
|
|
66
|
+
never implemented it.
|
|
67
|
+
- **Rolling conversation summarization.** Past a threshold, older turns are incrementally
|
|
68
|
+
summarized and injected alongside retrieved chunks.
|
|
69
|
+
- **Model aliasing.** `LOCALGATE_MODEL_ALIASES='{"fast":"phi4-mini"}'` — swap models without
|
|
70
|
+
touching clients.
|
|
71
|
+
- **Prompt caching** (opt-in). Identical prompts skip inference. Off by default because it
|
|
72
|
+
makes sampling deterministic.
|
|
73
|
+
- **Plugin system.** Backends are discovered via the `localgate.backends` entry point. The
|
|
74
|
+
built-ins use the same mechanism, so there's no privileged path a plugin can't take.
|
|
75
|
+
- **Production hardening.** Structured JSON logs with correlation IDs, Prometheus `/metrics`,
|
|
76
|
+
a liveness/readiness split, graceful shutdown, and fail-fast config validation — production
|
|
77
|
+
now refuses to start with the placeholder admin key.
|
|
78
|
+
- **`GET /admin/export`** — every row as JSON, so nobody is locked in.
|
|
79
|
+
- **Retrieval score logging and `MEMORY_MIN_SCORE`** — the floor that stops irrelevant chunks
|
|
80
|
+
from filling the context window with noise.
|
|
81
|
+
- **`key_prefix`** on keys, so a key can be identified in a listing.
|
|
82
|
+
- **`PATCH /admin/keys/{id}`** — change a rate limit without reissuing the key.
|
|
83
|
+
- **Documentation.** All seven docs were one-line stubs; they are now written, along with
|
|
84
|
+
three ADRs covering the decisions that shaped the codebase.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- `GET /v1/conversations/{id}` now returns an object (messages, summary, chunk count) rather
|
|
89
|
+
than a bare array.
|
|
90
|
+
- `POST /admin/keys` returns 201, not 200.
|
|
91
|
+
- Usage records now carry latency and a cached flag.
|
|
92
|
+
|
|
93
|
+
### Security
|
|
94
|
+
|
|
95
|
+
- SECURITY.md claimed keys were bcrypted; they are SHA-256, which is the *correct* choice for
|
|
96
|
+
high-entropy random keys verified on every request. The document now says so and explains
|
|
97
|
+
why ([ADR 0003](docs/decisions/0003-sha256-for-api-keys.md)).
|
|
98
|
+
|
|
99
|
+
## [0.2.0] — 2026-07-13
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
- Auth, token accounting, RAG memory, dashboard UI, established-database config.
|
|
103
|
+
- Test suite with a deterministic `FakeBackend`.
|
|
104
|
+
|
|
105
|
+
## [0.1.0] — 2026-07-13
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
- Initial scaffolding: FastAPI app, Ollama backend, `/v1/chat/completions`, config, CI.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for wanting to help. This is meant to be an easy project to contribute to — the
|
|
4
|
+
architecture is deliberately shaped so that the most common contributions (a new backend, a
|
|
5
|
+
new database) are one file each.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/YOUR_USERNAME/localgate.git
|
|
11
|
+
cd localgate
|
|
12
|
+
uv sync --all-extras
|
|
13
|
+
uv run pre-commit install
|
|
14
|
+
|
|
15
|
+
make test # 160 tests, no live Ollama needed
|
|
16
|
+
make lint # ruff + mypy
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Tests run against a deterministic `FakeBackend` and an in-memory SQLite database, so the
|
|
20
|
+
suite is fast and needs nothing installed.
|
|
21
|
+
|
|
22
|
+
## The codebase in one minute
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
api/ routes — thin. Validate, delegate, respond. Never touch the database.
|
|
26
|
+
core/ business logic — auth, rate limiting, tokens, caching. No HTTP awareness.
|
|
27
|
+
backends/ one adapter per inference server.
|
|
28
|
+
memory/ chunking, embedding, retrieval, summarization.
|
|
29
|
+
db/ models + repositories. The only code that writes SQL.
|
|
30
|
+
middleware/ request id, access log, metrics.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Read [docs/architecture.md](docs/architecture.md) before a substantial change, and the
|
|
34
|
+
[ADRs](docs/decisions/) for *why* things are the way they are. If you're about to change
|
|
35
|
+
something an ADR covers, that's fine — but say so in the PR, and update the ADR.
|
|
36
|
+
|
|
37
|
+
## Adding a backend
|
|
38
|
+
|
|
39
|
+
This is the highest-value contribution and it's genuinely small. If the server speaks the
|
|
40
|
+
OpenAI API — most do — it's a subclass and a default port:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# src/localgate/backends/my_server.py
|
|
44
|
+
from localgate.backends.openai_compat import OpenAICompatBackend
|
|
45
|
+
|
|
46
|
+
class MyServerBackend(OpenAICompatBackend):
|
|
47
|
+
name = "my-server"
|
|
48
|
+
default_base_url = "http://localhost:9000"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Register it in `pyproject.toml`:
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[project.entry-points."localgate.backends"]
|
|
55
|
+
my-server = "localgate.backends.my_server:MyServerBackend"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Add a case to `tests/unit/test_backends.py::test_every_advertised_backend_can_actually_be_instantiated`.
|
|
59
|
+
That's it.
|
|
60
|
+
|
|
61
|
+
If the server *doesn't* speak OpenAI, implement `InferenceBackend` directly (see
|
|
62
|
+
`backends/base.py` — five methods) and translate in the adapter. Nothing above the backend
|
|
63
|
+
layer should ever learn that your server is different.
|
|
64
|
+
|
|
65
|
+
You can also ship a backend as **your own package** without touching this repo at all —
|
|
66
|
+
declare the same entry point in your `pyproject.toml`. See
|
|
67
|
+
[docs/architecture.md](docs/architecture.md#plugin-system).
|
|
68
|
+
|
|
69
|
+
## Standards
|
|
70
|
+
|
|
71
|
+
- **Type hints** on every function signature. `mypy src/` must pass.
|
|
72
|
+
- **Comments explain *why*, never *what*.** The code already says what it does. A comment
|
|
73
|
+
earns its place by capturing the constraint, the tradeoff, or the bug that isn't visible
|
|
74
|
+
from reading the line. If it restates the code, delete it.
|
|
75
|
+
- **Tests for every change.** A bug fix without a test that fails before it is not a fix.
|
|
76
|
+
- **`ruff check` and `ruff format`** — pre-commit runs both.
|
|
77
|
+
- **No `print`.** Use the structlog logger (`core/logging.py`).
|
|
78
|
+
|
|
79
|
+
## Tests
|
|
80
|
+
|
|
81
|
+
Write the test that would have caught the bug. The ones in here that earn their keep are:
|
|
82
|
+
|
|
83
|
+
- `test_every_route_is_guarded` — auth is per-route, so a new route that forgets its
|
|
84
|
+
dependency would be silently public. This catches that.
|
|
85
|
+
- `test_pre_migrations_database_is_adopted_without_losing_data` — the upgrade path for
|
|
86
|
+
existing users, which is exactly the thing you can't test by hand twice.
|
|
87
|
+
- `test_streaming_still_records_usage_after_the_body_is_sent` — works in dev, breaks in
|
|
88
|
+
prod, if you get the session lifecycle wrong.
|
|
89
|
+
|
|
90
|
+
Aim for that. Not coverage of lines, coverage of *failure modes*.
|
|
91
|
+
|
|
92
|
+
## Pull requests
|
|
93
|
+
|
|
94
|
+
1. Branch from `main`: `git checkout -b feat/my-thing`
|
|
95
|
+
2. Make the change, with tests.
|
|
96
|
+
3. `make lint && make test`
|
|
97
|
+
4. Update `CHANGELOG.md` under `[Unreleased]`.
|
|
98
|
+
5. Open the PR and fill in the template.
|
|
99
|
+
|
|
100
|
+
Small PRs get reviewed fast. A 2000-line PR touching six subsystems will sit.
|
|
101
|
+
|
|
102
|
+
## Good first issues
|
|
103
|
+
|
|
104
|
+
Look for the `good-first-issue` label. Reliably useful things that are always open:
|
|
105
|
+
|
|
106
|
+
- A backend adapter for a server that isn't supported yet
|
|
107
|
+
- Better error messages — if something confused you, it will confuse the next person, and
|
|
108
|
+
the fix is a sentence
|
|
109
|
+
- Documentation gaps, especially in [getting-started](docs/getting-started.md)
|
|
110
|
+
- Anything in the [open issues](https://github.com/AnjalLLL/localgate/issues)
|
|
111
|
+
|
|
112
|
+
## Code of Conduct
|
|
113
|
+
|
|
114
|
+
This project follows the [Code of Conduct](CODE_OF_CONDUCT.md). It applies everywhere the
|
|
115
|
+
project does.
|