docugraph 0.1.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.
- docugraph-0.1.0/.dockerignore +63 -0
- docugraph-0.1.0/.github/dependabot.yml +101 -0
- docugraph-0.1.0/.github/workflows/ci.yml +210 -0
- docugraph-0.1.0/.github/workflows/docker-publish.yml +85 -0
- docugraph-0.1.0/.github/workflows/live-provider-smoke.yml +77 -0
- docugraph-0.1.0/.github/workflows/live-reranker.yml +91 -0
- docugraph-0.1.0/.github/workflows/release.yml +124 -0
- docugraph-0.1.0/.gitignore +13 -0
- docugraph-0.1.0/.pre-commit-config.yaml +62 -0
- docugraph-0.1.0/.python-version +1 -0
- docugraph-0.1.0/LICENSE +201 -0
- docugraph-0.1.0/NOTICE +10 -0
- docugraph-0.1.0/PKG-INFO +354 -0
- docugraph-0.1.0/README.md +296 -0
- docugraph-0.1.0/claude-integration/README.md +314 -0
- docugraph-0.1.0/claude-integration/hooks/docugraph-auto-index.sh +79 -0
- docugraph-0.1.0/claude-integration/hooks/docugraph-auto-search.sh +56 -0
- docugraph-0.1.0/claude-integration/hooks/docugraph-post-compact.sh +151 -0
- docugraph-0.1.0/claude-integration/hooks/docugraph-pre-compact.sh +157 -0
- docugraph-0.1.0/claude-integration/hooks/docugraph-rule-check.sh +93 -0
- docugraph-0.1.0/claude-integration/hooks/docugraph-session-end.sh +86 -0
- docugraph-0.1.0/claude-integration/hooks/docugraph-session-start.sh +44 -0
- docugraph-0.1.0/claude-integration/install.sh +113 -0
- docugraph-0.1.0/claude-integration/settings.json +88 -0
- docugraph-0.1.0/claude-integration/skills/architecture/SKILL.md +138 -0
- docugraph-0.1.0/claude-integration/skills/context/SKILL.md +120 -0
- docugraph-0.1.0/claude-integration/skills/docs/SKILL.md +70 -0
- docugraph-0.1.0/claude-integration/skills/index/SKILL.md +65 -0
- docugraph-0.1.0/claude-integration/skills/recall/SKILL.md +76 -0
- docugraph-0.1.0/claude-integration/skills/remember/SKILL.md +68 -0
- docugraph-0.1.0/docker/Dockerfile +111 -0
- docugraph-0.1.0/docker/docker-compose.yml +46 -0
- docugraph-0.1.0/docker-compose.yml +22 -0
- docugraph-0.1.0/docs/SECURITY.md +45 -0
- docugraph-0.1.0/docs/configuration.md +316 -0
- docugraph-0.1.0/docs/quickstart.md +262 -0
- docugraph-0.1.0/examples/crawl_and_search.py +118 -0
- docugraph-0.1.0/examples/index_local_repo.py +114 -0
- docugraph-0.1.0/examples/mcp_client_example.py +130 -0
- docugraph-0.1.0/pyproject.toml +223 -0
- docugraph-0.1.0/scripts/live_provider_smoke.py +138 -0
- docugraph-0.1.0/src/docugraph/__init__.py +24 -0
- docugraph-0.1.0/src/docugraph/_version.py +28 -0
- docugraph-0.1.0/src/docugraph/agents/__init__.py +27 -0
- docugraph-0.1.0/src/docugraph/agents/coding_agent.py +532 -0
- docugraph-0.1.0/src/docugraph/agents/tools/__init__.py +62 -0
- docugraph-0.1.0/src/docugraph/agents/tools/graph_query.py +203 -0
- docugraph-0.1.0/src/docugraph/agents/tools/memory_ops.py +305 -0
- docugraph-0.1.0/src/docugraph/agents/tools/search_code.py +305 -0
- docugraph-0.1.0/src/docugraph/agents/tools/search_docs.py +148 -0
- docugraph-0.1.0/src/docugraph/core/__init__.py +34 -0
- docugraph-0.1.0/src/docugraph/core/config.py +183 -0
- docugraph-0.1.0/src/docugraph/core/embeddings.py +394 -0
- docugraph-0.1.0/src/docugraph/core/llm.py +892 -0
- docugraph-0.1.0/src/docugraph/core/models.py +145 -0
- docugraph-0.1.0/src/docugraph/ingestion/__init__.py +64 -0
- docugraph-0.1.0/src/docugraph/ingestion/api_docs.py +646 -0
- docugraph-0.1.0/src/docugraph/ingestion/chunker.py +382 -0
- docugraph-0.1.0/src/docugraph/ingestion/crawler.py +305 -0
- docugraph-0.1.0/src/docugraph/ingestion/git_indexer.py +556 -0
- docugraph-0.1.0/src/docugraph/ingestion/local_files.py +516 -0
- docugraph-0.1.0/src/docugraph/interfaces/__init__.py +1 -0
- docugraph-0.1.0/src/docugraph/interfaces/cli.py +983 -0
- docugraph-0.1.0/src/docugraph/interfaces/mcp_server.py +918 -0
- docugraph-0.1.0/src/docugraph/retrieval/__init__.py +54 -0
- docugraph-0.1.0/src/docugraph/retrieval/context_builder.py +522 -0
- docugraph-0.1.0/src/docugraph/retrieval/hybrid_search.py +534 -0
- docugraph-0.1.0/src/docugraph/retrieval/reranker.py +532 -0
- docugraph-0.1.0/src/docugraph/storage/__init__.py +14 -0
- docugraph-0.1.0/src/docugraph/storage/graph_store.py +602 -0
- docugraph-0.1.0/src/docugraph/storage/memory_store.py +529 -0
- docugraph-0.1.0/src/docugraph/storage/vector_store.py +367 -0
- docugraph-0.1.0/tests/e2e/__init__.py +1 -0
- docugraph-0.1.0/tests/e2e/test_cli.py +159 -0
- docugraph-0.1.0/tests/e2e/test_hooks.py +139 -0
- docugraph-0.1.0/tests/e2e/test_mcp_server.py +189 -0
- docugraph-0.1.0/tests/e2e/tools_snapshot.json +262 -0
- docugraph-0.1.0/tests/integration/__init__.py +1 -0
- docugraph-0.1.0/tests/integration/test_graph_store_init.py +137 -0
- docugraph-0.1.0/tests/integration/test_memory_store.py +152 -0
- docugraph-0.1.0/tests/unit/__init__.py +1 -0
- docugraph-0.1.0/tests/unit/test_chunker.py +180 -0
- docugraph-0.1.0/tests/unit/test_cloud_provider_sdks.py +372 -0
- docugraph-0.1.0/tests/unit/test_docs_consistency.py +441 -0
- docugraph-0.1.0/tests/unit/test_embeddings.py +185 -0
- docugraph-0.1.0/tests/unit/test_event_loop_acquisition.py +376 -0
- docugraph-0.1.0/tests/unit/test_graph_embedder_adapter.py +66 -0
- docugraph-0.1.0/tests/unit/test_graphiti_llm_client.py +75 -0
- docugraph-0.1.0/tests/unit/test_hybrid_graph_status.py +215 -0
- docugraph-0.1.0/tests/unit/test_llm_preflight.py +355 -0
- docugraph-0.1.0/tests/unit/test_models.py +306 -0
- docugraph-0.1.0/tests/unit/test_no_model_ids_in_source.py +205 -0
- docugraph-0.1.0/tests/unit/test_reranker.py +185 -0
- docugraph-0.1.0/tests/unit/test_vector_store.py +94 -0
- docugraph-0.1.0/tests/unit/test_version.py +67 -0
- docugraph-0.1.0/uv.lock +3742 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Build context exclusions. The Dockerfile copies narrowly today
|
|
2
|
+
# (`pyproject.toml README.md` and `src/`), so nothing here reaches the image --
|
|
3
|
+
# but the whole tree is still sent to the daemon, and a single future
|
|
4
|
+
# `COPY . .` would otherwise carry a 1.1 GB .venv or a .env into a published
|
|
5
|
+
# image. This file is what makes that copy safe rather than lucky.
|
|
6
|
+
#
|
|
7
|
+
# Docker matches these against the path relative to the context root, so a bare
|
|
8
|
+
# name covers only the root -- `__pycache__` does not match `src/x/__pycache__`.
|
|
9
|
+
# Depth-capable patterns therefore carry `**/`. Patterns that are meaningfully
|
|
10
|
+
# root-only (.venv, .git, data) stay anchored, so a nested `tests/fixtures/data`
|
|
11
|
+
# is not swept up with them.
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# Secrets. Nothing here is tracked, but an untracked .env is exactly what a
|
|
19
|
+
# broad COPY would pick up -- at any depth.
|
|
20
|
+
**/.env
|
|
21
|
+
**/.env.*
|
|
22
|
+
|
|
23
|
+
# Key material
|
|
24
|
+
**/*.pem
|
|
25
|
+
**/*.key
|
|
26
|
+
**/*.p12
|
|
27
|
+
**/id_rsa*
|
|
28
|
+
|
|
29
|
+
# VCS
|
|
30
|
+
.git/
|
|
31
|
+
.gitignore
|
|
32
|
+
|
|
33
|
+
# Caches, test and type-checker output
|
|
34
|
+
**/__pycache__/
|
|
35
|
+
**/*.py[cod]
|
|
36
|
+
**/.pytest_cache/
|
|
37
|
+
**/.ruff_cache/
|
|
38
|
+
**/.mypy_cache/
|
|
39
|
+
**/.cache/
|
|
40
|
+
**/htmlcov/
|
|
41
|
+
**/.coverage
|
|
42
|
+
**/.coverage.*
|
|
43
|
+
**/*.log
|
|
44
|
+
|
|
45
|
+
# Build output
|
|
46
|
+
build/
|
|
47
|
+
dist/
|
|
48
|
+
wheels/
|
|
49
|
+
**/*.egg-info/
|
|
50
|
+
|
|
51
|
+
# Local data stores. The container's data dir is a volume
|
|
52
|
+
# (DOCUGRAPH_STORAGE__DATA_DIR=/data/docugraph), never build input.
|
|
53
|
+
data/
|
|
54
|
+
**/*.kuzu
|
|
55
|
+
**/*.lance
|
|
56
|
+
**/*.db
|
|
57
|
+
|
|
58
|
+
# OS / editor noise
|
|
59
|
+
**/.DS_Store
|
|
60
|
+
|
|
61
|
+
# tests/, docs/ and examples/ are deliberately NOT excluded: the `development`
|
|
62
|
+
# target installs `.[dev]` and Playwright, so the suite is meant to be runnable
|
|
63
|
+
# inside that container.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Dependabot is configured rather than switched off.
|
|
2
|
+
#
|
|
3
|
+
# Left as it was, this file opened a PR per dependency per week, which in practice
|
|
4
|
+
# meant a queue of PRs that could not be merged. Five of docugraph's dependencies
|
|
5
|
+
# are held deliberately, and the old grouping covered minor and patch bumps only
|
|
6
|
+
# -- so every major bump, which is exactly where the held-back lines were, arrived
|
|
7
|
+
# as its own PR proposing to lift them. Eight were open at the time of writing.
|
|
8
|
+
#
|
|
9
|
+
# Now the held dependencies are ignored by name with the reason recorded beside
|
|
10
|
+
# each, and the rest arrives as one grouped PR per ecosystem per month: a size a
|
|
11
|
+
# person can review in one sitting.
|
|
12
|
+
#
|
|
13
|
+
# These `ignore` entries restate decisions already written into pyproject.toml.
|
|
14
|
+
# If a pin is lifted there, lift it here too -- otherwise this file goes on
|
|
15
|
+
# suppressing updates that nobody still means to suppress. Note that Dependabot
|
|
16
|
+
# reads this file only from the default branch, so changes here take effect at
|
|
17
|
+
# the next merge to `main`, not at the push.
|
|
18
|
+
#
|
|
19
|
+
# There are deliberately no `labels:` entries. The previous config asked for
|
|
20
|
+
# `dependencies`, `python`, `github-actions` and `docker`, none of which exist in
|
|
21
|
+
# this repository, so every Dependabot PR carried "the following labels could not
|
|
22
|
+
# be found" and no labels at all. Either create them and put the entries back, or
|
|
23
|
+
# leave this as it is; asking for labels that do not exist is worse than not
|
|
24
|
+
# asking.
|
|
25
|
+
|
|
26
|
+
version: 2
|
|
27
|
+
|
|
28
|
+
updates:
|
|
29
|
+
# ------------------------------------------------------------------ Python deps
|
|
30
|
+
- package-ecosystem: "pip"
|
|
31
|
+
directory: "/"
|
|
32
|
+
schedule:
|
|
33
|
+
interval: "monthly"
|
|
34
|
+
commit-message:
|
|
35
|
+
prefix: "deps"
|
|
36
|
+
groups:
|
|
37
|
+
pip:
|
|
38
|
+
patterns: ["*"]
|
|
39
|
+
ignore:
|
|
40
|
+
# Pinned exactly in pyproject.toml. kuzu is archived upstream -- 0.11.3 is the
|
|
41
|
+
# final release -- and its file format is not stable before 1.0, so a bump can
|
|
42
|
+
# leave an existing graph unreadable. Lifting this pin needs a migration step,
|
|
43
|
+
# not a dependency bump.
|
|
44
|
+
- dependency-name: "kuzu"
|
|
45
|
+
|
|
46
|
+
# Pinned exactly in pyproject.toml, so that a graphiti release cannot change
|
|
47
|
+
# its kuzu requirement or drop the kuzu driver and break the pin above, with
|
|
48
|
+
# no change in this repo to explain it. Bump only alongside a re-run of the
|
|
49
|
+
# ingestion tests.
|
|
50
|
+
- dependency-name: "graphiti-core"
|
|
51
|
+
|
|
52
|
+
# Capped below 2 in pyproject.toml: the 2.x SDK removed the low-level Server
|
|
53
|
+
# list_tools/call_tool decorators that mcp_server.py is built on.
|
|
54
|
+
- dependency-name: "mcp"
|
|
55
|
+
versions: [">= 2.0.0"]
|
|
56
|
+
|
|
57
|
+
# Capped below 1.24: newer releases dropped macOS x86_64 wheels, so the
|
|
58
|
+
# install breaks on an Intel Mac. Same reason as lancedb below.
|
|
59
|
+
- dependency-name: "onnxruntime"
|
|
60
|
+
versions: [">= 1.24.0"]
|
|
61
|
+
|
|
62
|
+
# Capped below 0.26, for the same dropped-wheel reason.
|
|
63
|
+
- dependency-name: "lancedb"
|
|
64
|
+
versions: [">= 0.26.0"]
|
|
65
|
+
|
|
66
|
+
# The three cloud SDKs are deliberately *not* ignored here, though they
|
|
67
|
+
# are still capped in pyproject.toml. They were, and it was the wrong call:
|
|
68
|
+
# an ignore suppresses the PR that would have shown you the break. The
|
|
69
|
+
# Anthropic 1.x SDK removed `temperature` from its request type and every
|
|
70
|
+
# call raised `unexpected keyword argument` -- and because the cap arrived
|
|
71
|
+
# by hand, the break was discovered rather than announced. A major bump for
|
|
72
|
+
# any of these now opens a PR that proposes lifting the cap, and CI's
|
|
73
|
+
# all-extras job runs tests/unit/test_cloud_provider_sdks.py against the
|
|
74
|
+
# proposed version, which is the only thing that can say whether the new
|
|
75
|
+
# SDK still accepts the arguments these providers pass. That job exists for
|
|
76
|
+
# this: read its result before merging, because a green install proves
|
|
77
|
+
# nothing about the request shape.
|
|
78
|
+
|
|
79
|
+
# ----------------------------------------------------------------- GitHub Actions
|
|
80
|
+
- package-ecosystem: "github-actions"
|
|
81
|
+
directory: "/"
|
|
82
|
+
schedule:
|
|
83
|
+
interval: "monthly"
|
|
84
|
+
commit-message:
|
|
85
|
+
prefix: "ci"
|
|
86
|
+
groups:
|
|
87
|
+
github-actions:
|
|
88
|
+
patterns: ["*"]
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------ Docker images
|
|
91
|
+
# Kept from the previous config, now monthly like the others. Nothing is held
|
|
92
|
+
# here, so it is a plain grouped update of the base image.
|
|
93
|
+
- package-ecosystem: "docker"
|
|
94
|
+
directory: "/docker"
|
|
95
|
+
schedule:
|
|
96
|
+
interval: "monthly"
|
|
97
|
+
commit-message:
|
|
98
|
+
prefix: "docker"
|
|
99
|
+
groups:
|
|
100
|
+
docker:
|
|
101
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Continuous Integration - Run tests and linting
|
|
2
|
+
name: CI
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
- master
|
|
9
|
+
# Deliberately no branch filter. With `branches: [main, master]` here, a pull
|
|
10
|
+
# request whose base is any other branch -- which is how a change is stacked on
|
|
11
|
+
# another unmerged change -- got no CI at all, and "no checks reported" in the
|
|
12
|
+
# PR list reads much like "checks passed". Every pull request runs CI.
|
|
13
|
+
pull_request:
|
|
14
|
+
|
|
15
|
+
env:
|
|
16
|
+
# fastembed model cache (bge-small ~130 MB; shared across jobs)
|
|
17
|
+
FASTEMBED_CACHE_PATH: /home/runner/.cache/fastembed
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
# The workflow files are the one thing here with no other gate, and release.yml
|
|
21
|
+
# is the part that runs rarely and matters most -- so a mistake in it is found
|
|
22
|
+
# at release time, by everyone. actionlint reads the workflows themselves:
|
|
23
|
+
# expression syntax, job-graph references, and the shell and Python inside each
|
|
24
|
+
# `run:` block, through shellcheck and pyflakes, both of which the pinned image
|
|
25
|
+
# below ships. Run against this tree it reported two SC2086 findings in
|
|
26
|
+
# release.yml (`>> $GITHUB_OUTPUT` unquoted); both are fixed.
|
|
27
|
+
actionlint:
|
|
28
|
+
name: Lint workflows
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- name: Checkout repository
|
|
32
|
+
uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Run actionlint
|
|
35
|
+
uses: docker://rhysd/actionlint:1.7.12
|
|
36
|
+
with:
|
|
37
|
+
args: -color
|
|
38
|
+
|
|
39
|
+
lint:
|
|
40
|
+
name: Lint
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- name: Checkout repository
|
|
44
|
+
uses: actions/checkout@v4
|
|
45
|
+
|
|
46
|
+
- name: Set up Python
|
|
47
|
+
uses: actions/setup-python@v5
|
|
48
|
+
with:
|
|
49
|
+
python-version: '3.11'
|
|
50
|
+
|
|
51
|
+
- name: Install uv
|
|
52
|
+
uses: astral-sh/setup-uv@v4
|
|
53
|
+
|
|
54
|
+
# --all-extras, not --extra dev. The cloud SDKs are imported conditionally,
|
|
55
|
+
# so with them absent mypy reports "cannot find implementation or library
|
|
56
|
+
# stub" and types the calls against `Any` -- which is how a `temperature`
|
|
57
|
+
# argument the Anthropic SDK had already removed passed type checking while
|
|
58
|
+
# raising TypeError at runtime for every user of docugraph[cloud].
|
|
59
|
+
- name: Sync dependencies
|
|
60
|
+
run: uv sync --all-extras
|
|
61
|
+
|
|
62
|
+
# `scripts/` is included, not just `src/`. The live-provider smoke script is
|
|
63
|
+
# not part of the package but it is part of this repository, and code no gate
|
|
64
|
+
# reads is code that rots. mypy gets both paths together so it resolves
|
|
65
|
+
# `docugraph` to this source tree rather than to the installed copy, which it
|
|
66
|
+
# otherwise reports as an untyped library.
|
|
67
|
+
- name: Run Ruff linter
|
|
68
|
+
run: uv run ruff check src/ scripts/
|
|
69
|
+
|
|
70
|
+
- name: Run Ruff formatter check
|
|
71
|
+
run: uv run ruff format --check src/ scripts/
|
|
72
|
+
|
|
73
|
+
- name: Run mypy
|
|
74
|
+
run: uv run mypy src/ scripts/
|
|
75
|
+
|
|
76
|
+
# These SDKs are installed in this job and nowhere else in CI, so it is the
|
|
77
|
+
# only place the cloud request shapes can be checked against the real
|
|
78
|
+
# libraries rather than against their absence. Skips itself if anthropic
|
|
79
|
+
# is missing, which is why it belongs to the all-extras job.
|
|
80
|
+
- name: Check the cloud provider calls against the installed SDKs
|
|
81
|
+
run: uv run pytest tests/unit/test_cloud_provider_sdks.py -v
|
|
82
|
+
|
|
83
|
+
test:
|
|
84
|
+
name: Test (py${{ matrix.python-version }})
|
|
85
|
+
runs-on: ubuntu-latest
|
|
86
|
+
needs: lint
|
|
87
|
+
strategy:
|
|
88
|
+
# Report every version, rather than cancelling the others when one fails.
|
|
89
|
+
fail-fast: false
|
|
90
|
+
matrix:
|
|
91
|
+
# Every version the package claims, via the pyproject classifiers, now
|
|
92
|
+
# that 3.13 is among them. 3.14 is excluded by `requires-python`; the
|
|
93
|
+
# comment there gives the two independent reasons.
|
|
94
|
+
python-version: ['3.11', '3.12', '3.13']
|
|
95
|
+
env:
|
|
96
|
+
# uv otherwise reads .python-version (3.11) and silently builds a 3.11
|
|
97
|
+
# venv for both legs, so the matrix would prove nothing. This overrides
|
|
98
|
+
# that file for every uv command in the job, `uv run` included.
|
|
99
|
+
UV_PYTHON: ${{ matrix.python-version }}
|
|
100
|
+
|
|
101
|
+
steps:
|
|
102
|
+
- name: Checkout repository
|
|
103
|
+
uses: actions/checkout@v4
|
|
104
|
+
|
|
105
|
+
- name: Set up Python
|
|
106
|
+
uses: actions/setup-python@v5
|
|
107
|
+
with:
|
|
108
|
+
python-version: ${{ matrix.python-version }}
|
|
109
|
+
|
|
110
|
+
- name: Install uv
|
|
111
|
+
uses: astral-sh/setup-uv@v4
|
|
112
|
+
|
|
113
|
+
# `--extra cloud` as well as dev, so the whole suite runs with the SDKs
|
|
114
|
+
# present. Without it the two `importorskip("openai")` tests in
|
|
115
|
+
# tests/unit/test_embeddings.py skipped here -- and they ran nowhere else,
|
|
116
|
+
# because the all-extras job runs only tests/unit/test_cloud_provider_sdks.py.
|
|
117
|
+
# One of the two is the guard on the auto-embedding fix: a key in the
|
|
118
|
+
# environment must not be read as a request to send documents to OpenAI.
|
|
119
|
+
# A regression guard that never runs is a comment.
|
|
120
|
+
#
|
|
121
|
+
# `ollama` is deliberately not included: no test imports it, and the three
|
|
122
|
+
# ollama tests in tests/unit/test_llm_preflight.py monkeypatch the probe.
|
|
123
|
+
- name: Sync dependencies
|
|
124
|
+
run: uv sync --extra dev --extra cloud
|
|
125
|
+
|
|
126
|
+
- name: Cache fastembed models
|
|
127
|
+
uses: actions/cache@v4
|
|
128
|
+
with:
|
|
129
|
+
path: /home/runner/.cache/fastembed
|
|
130
|
+
key: fastembed-bge-small-en-v1.5
|
|
131
|
+
|
|
132
|
+
- name: Run tests
|
|
133
|
+
run: uv run pytest tests/unit/ tests/integration/ tests/e2e/ -v --tb=short
|
|
134
|
+
|
|
135
|
+
docker:
|
|
136
|
+
# Builds the image but never publishes it: docker-publish.yml owns pushing,
|
|
137
|
+
# and that only triggers on a tag. This job exists so a dependency change
|
|
138
|
+
# cannot break the image without anyone noticing between releases.
|
|
139
|
+
name: Docker (production image)
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
needs: lint
|
|
142
|
+
|
|
143
|
+
steps:
|
|
144
|
+
- name: Checkout repository
|
|
145
|
+
uses: actions/checkout@v4
|
|
146
|
+
|
|
147
|
+
- name: Build the production target
|
|
148
|
+
run: docker build --file docker/Dockerfile --target production --tag docugraph:ci .
|
|
149
|
+
|
|
150
|
+
- name: Handshake over stdio
|
|
151
|
+
# Initialise, then let stdin close. A missing LLM does not stop the
|
|
152
|
+
# server from identifying itself -- that is the point of the
|
|
153
|
+
# degradation work -- so this needs no credentials.
|
|
154
|
+
run: |
|
|
155
|
+
printf '%s\n' \
|
|
156
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci-handshake","version":"0"}}}' \
|
|
157
|
+
| timeout 300 docker run -i --rm docugraph:ci \
|
|
158
|
+
> /tmp/handshake.jsonl 2> /tmp/handshake.err \
|
|
159
|
+
|| echo "docker run exit: $?"
|
|
160
|
+
echo "--- container stderr (tail) ---"
|
|
161
|
+
tail -20 /tmp/handshake.err || true
|
|
162
|
+
|
|
163
|
+
- name: Assert the handshake
|
|
164
|
+
run: |
|
|
165
|
+
python3 - <<'PY'
|
|
166
|
+
import json, pathlib, sys
|
|
167
|
+
|
|
168
|
+
EXPECTED = "0.1.0" # the version this repo publishes; pinned deliberately
|
|
169
|
+
raw = pathlib.Path("/tmp/handshake.jsonl").read_text()
|
|
170
|
+
|
|
171
|
+
# stdout carries the JSON-RPC stream and nothing else. A stray line
|
|
172
|
+
# here corrupts the protocol, so it fails rather than being skipped.
|
|
173
|
+
messages = []
|
|
174
|
+
for n, line in enumerate(raw.splitlines(), 1):
|
|
175
|
+
if not line.strip():
|
|
176
|
+
continue
|
|
177
|
+
try:
|
|
178
|
+
messages.append(json.loads(line))
|
|
179
|
+
except json.JSONDecodeError as e:
|
|
180
|
+
sys.exit(f"stdout line {n} is not JSON: {line[:160]!r} ({e})")
|
|
181
|
+
|
|
182
|
+
infos = [
|
|
183
|
+
m["result"]["serverInfo"]
|
|
184
|
+
for m in messages
|
|
185
|
+
if isinstance(m.get("result"), dict) and "serverInfo" in m["result"]
|
|
186
|
+
]
|
|
187
|
+
if not infos:
|
|
188
|
+
sys.exit(f"no serverInfo in {len(messages)} message(s): {messages!r}")
|
|
189
|
+
if infos[0].get("version") != EXPECTED:
|
|
190
|
+
sys.exit(f"serverInfo.version is {infos[0].get('version')!r}, expected {EXPECTED!r}")
|
|
191
|
+
|
|
192
|
+
print(f"stdout: {len(messages)} JSON-RPC message(s), 0 stray lines")
|
|
193
|
+
print(f"serverInfo: {infos[0]}")
|
|
194
|
+
PY
|
|
195
|
+
|
|
196
|
+
test-summary:
|
|
197
|
+
name: Test Summary
|
|
198
|
+
runs-on: ubuntu-latest
|
|
199
|
+
needs: test
|
|
200
|
+
if: always()
|
|
201
|
+
|
|
202
|
+
steps:
|
|
203
|
+
- name: Check test results
|
|
204
|
+
run: |
|
|
205
|
+
if [ "${{ needs.test.result }}" == "success" ]; then
|
|
206
|
+
echo "All tests passed!"
|
|
207
|
+
else
|
|
208
|
+
echo "Some tests failed"
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Build and publish Docker images to GitHub Container Registry
|
|
2
|
+
name: Docker Publish
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
# Manual only. Publishing a release goes through release.yml, which builds the
|
|
6
|
+
# same production image and tags it with the release version; a `v*` tag used
|
|
7
|
+
# to fire this file as well, building that image a second time under a second
|
|
8
|
+
# set of tags. Branch pushes deliberately do not trigger it either: an image
|
|
9
|
+
# for every push to main is noise, and it made the first push to a new
|
|
10
|
+
# repository fail a multi-platform build before anything was released.
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
REGISTRY: ghcr.io
|
|
15
|
+
IMAGE_NAME: ${{ github.repository }}
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build-and-push:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
packages: write
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout repository
|
|
26
|
+
uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
# The arm64 half of the multi-platform build runs under emulation on the
|
|
29
|
+
# amd64 runners, which requires QEMU registered with binfmt. Without this
|
|
30
|
+
# step the arm64 build fails.
|
|
31
|
+
- name: Set up QEMU
|
|
32
|
+
uses: docker/setup-qemu-action@v3
|
|
33
|
+
|
|
34
|
+
- name: Set up Docker Buildx
|
|
35
|
+
uses: docker/setup-buildx-action@v3
|
|
36
|
+
|
|
37
|
+
- name: Log in to Container Registry
|
|
38
|
+
uses: docker/login-action@v3
|
|
39
|
+
with:
|
|
40
|
+
registry: ${{ env.REGISTRY }}
|
|
41
|
+
username: ${{ github.actor }}
|
|
42
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
|
|
44
|
+
- name: Extract metadata (tags, labels)
|
|
45
|
+
id: meta
|
|
46
|
+
uses: docker/metadata-action@v5
|
|
47
|
+
with:
|
|
48
|
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
49
|
+
tags: |
|
|
50
|
+
# The tag as pushed (v1.2.3)
|
|
51
|
+
type=ref,event=tag
|
|
52
|
+
# Semver components (1.2.3, 1.2, 1)
|
|
53
|
+
type=semver,pattern={{version}}
|
|
54
|
+
type=semver,pattern={{major}}.{{minor}}
|
|
55
|
+
type=semver,pattern={{major}}
|
|
56
|
+
# Short SHA, so a manual run is traceable
|
|
57
|
+
type=sha,prefix=sha-
|
|
58
|
+
# `latest` only for a version tag, never for a manual run
|
|
59
|
+
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
60
|
+
|
|
61
|
+
- name: Build and push Docker image
|
|
62
|
+
uses: docker/build-push-action@v5
|
|
63
|
+
with:
|
|
64
|
+
context: .
|
|
65
|
+
file: ./docker/Dockerfile
|
|
66
|
+
target: production
|
|
67
|
+
push: true
|
|
68
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
69
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
70
|
+
cache-from: type=gha
|
|
71
|
+
cache-to: type=gha,mode=max
|
|
72
|
+
platforms: linux/amd64,linux/arm64
|
|
73
|
+
|
|
74
|
+
- name: Build and push development image
|
|
75
|
+
uses: docker/build-push-action@v5
|
|
76
|
+
with:
|
|
77
|
+
context: .
|
|
78
|
+
file: ./docker/Dockerfile
|
|
79
|
+
target: development
|
|
80
|
+
push: true
|
|
81
|
+
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
|
82
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
83
|
+
cache-from: type=gha
|
|
84
|
+
cache-to: type=gha,mode=max
|
|
85
|
+
platforms: linux/amd64,linux/arm64
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# One real call per cloud provider, weekly and on demand.
|
|
2
|
+
#
|
|
3
|
+
# This is the only job in this repository that talks to a provider for real.
|
|
4
|
+
# Everything else runs offline, and the unit tests that cover request shape use a
|
|
5
|
+
# mocked transport -- deliberately, because that is what makes them fast and
|
|
6
|
+
# deterministic, and also what makes them unable to answer the one question only
|
|
7
|
+
# a live call can: does the model the operator configured still exist, and does
|
|
8
|
+
# the provider still accept the request this package builds for it? Vendors
|
|
9
|
+
# retire model ids on their own schedule, and no amount of local testing notices.
|
|
10
|
+
#
|
|
11
|
+
# So it is deliberately not in the push and PR gate. It needs secrets, it spends a
|
|
12
|
+
# fraction of a cent, and it fails for reasons that have nothing to do with the
|
|
13
|
+
# commit under test -- a red X on every PR would train people to ignore it. It
|
|
14
|
+
# runs on `main` only, weekly and by hand, and its failures are a signal about the
|
|
15
|
+
# world rather than about a diff.
|
|
16
|
+
#
|
|
17
|
+
# Models come from repository *variables* (Settings -> Secrets and variables
|
|
18
|
+
# -> Actions -> Variables): OPENAI_MODEL, ANTHROPIC_MODEL, COHERE_MODEL.
|
|
19
|
+
# A model id is not a credential, and being able to read it in the
|
|
20
|
+
# settings page is what makes a failure diagnosable.
|
|
21
|
+
# Keys come from repository *secrets* (OPENAI_API_KEY, ANTHROPIC_API_KEY,
|
|
22
|
+
# COHERE_API_KEY).
|
|
23
|
+
#
|
|
24
|
+
# A provider with no key or no model is reported as skipped, and the job stays
|
|
25
|
+
# green -- but a run where *nothing* was configured fails, because a smoke test
|
|
26
|
+
# that passes by testing nothing is worse than one that never ran.
|
|
27
|
+
name: Live provider smoke
|
|
28
|
+
|
|
29
|
+
on:
|
|
30
|
+
# Manual: Actions -> Live provider smoke -> Run workflow.
|
|
31
|
+
workflow_dispatch:
|
|
32
|
+
schedule:
|
|
33
|
+
# Mondays at 07:13 UTC. Off the hour on purpose: the top of the hour is when
|
|
34
|
+
# most of the world's scheduled jobs fire, and GitHub queues behind them.
|
|
35
|
+
- cron: '13 7 * * 1'
|
|
36
|
+
|
|
37
|
+
# Read-only: this job runs provider SDKs against a checked-out tree and writes
|
|
38
|
+
# nothing back.
|
|
39
|
+
permissions:
|
|
40
|
+
contents: read
|
|
41
|
+
|
|
42
|
+
jobs:
|
|
43
|
+
smoke:
|
|
44
|
+
name: One call per provider
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
# `main` only, including for a manual dispatch, which can otherwise be
|
|
47
|
+
# started from any branch. The point is to check what users have: a branch may
|
|
48
|
+
# name a model that is not the released default, and a green run there would
|
|
49
|
+
# say nothing about the published package. Scheduled runs are already pinned
|
|
50
|
+
# to the default branch.
|
|
51
|
+
if: github.ref == 'refs/heads/main'
|
|
52
|
+
steps:
|
|
53
|
+
- name: Checkout repository
|
|
54
|
+
uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- name: Set up Python
|
|
57
|
+
uses: actions/setup-python@v5
|
|
58
|
+
with:
|
|
59
|
+
python-version: '3.11'
|
|
60
|
+
|
|
61
|
+
- name: Install uv
|
|
62
|
+
uses: astral-sh/setup-uv@v4
|
|
63
|
+
|
|
64
|
+
# --all-extras so every provider's SDK is present; without it the ones that
|
|
65
|
+
# are missing report as skipped, which reads identically to unconfigured.
|
|
66
|
+
- name: Sync dependencies
|
|
67
|
+
run: uv sync --all-extras
|
|
68
|
+
|
|
69
|
+
- name: One tiny call per provider
|
|
70
|
+
env:
|
|
71
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
72
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
73
|
+
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
|
74
|
+
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
|
|
75
|
+
ANTHROPIC_MODEL: ${{ vars.ANTHROPIC_MODEL }}
|
|
76
|
+
COHERE_MODEL: ${{ vars.COHERE_MODEL }}
|
|
77
|
+
run: uv run python scripts/live_provider_smoke.py
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# The opt-in cross-encoder, run against the real weights, weekly and on demand.
|
|
2
|
+
#
|
|
3
|
+
# `tests/unit/test_reranker.py` is skipped on every push and pull request,
|
|
4
|
+
# because bge-reranker-v2-m3 is ~2.3 GB and a download on every run would make
|
|
5
|
+
# the fast offline suite slow and flaky. The consequence was that nothing ever
|
|
6
|
+
# exercised the real model: the one test that proves registration, download
|
|
7
|
+
# wiring and inference work together ran *never*. A gate that has never once
|
|
8
|
+
# been satisfied is not a gate; it is a comment.
|
|
9
|
+
#
|
|
10
|
+
# So this job runs it -- weekly, on `main`, with the weights cached between
|
|
11
|
+
# runs. It is deliberately separate from `live-provider-smoke.yml`, which talks
|
|
12
|
+
# to a cloud provider and says so in its header; this job talks to the Hugging
|
|
13
|
+
# Face hub and runs a model locally, and keeping them apart keeps each header
|
|
14
|
+
# true. The skip in the test file stays, because CI on a pull request must not
|
|
15
|
+
# download 2.3 GB.
|
|
16
|
+
#
|
|
17
|
+
# A *skip* fails this job. The weights are fetched in the step above the test,
|
|
18
|
+
# so nothing here can legitimately skip; if the test does skip, the cache is
|
|
19
|
+
# not where the test looks, and green would mean "checked nothing".
|
|
20
|
+
name: Live reranker
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
# Manual: Actions -> Live reranker -> Run workflow.
|
|
24
|
+
workflow_dispatch:
|
|
25
|
+
schedule:
|
|
26
|
+
# Tuesdays at 07:41 UTC. Off the hour, and the day after the provider smoke,
|
|
27
|
+
# so a bad Monday there does not hide a bad Tuesday here.
|
|
28
|
+
- cron: '41 7 * * 2'
|
|
29
|
+
|
|
30
|
+
# Read-only: this job downloads weights, runs a model and writes nothing back.
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
reranker:
|
|
36
|
+
name: Real cross-encoder inference
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
# `main` only, including for a manual dispatch, which can otherwise be
|
|
39
|
+
# started from any branch. The point is to check what users run: a branch
|
|
40
|
+
# may name a different model, and a green run there would say nothing about
|
|
41
|
+
# the released default. Scheduled runs are already pinned to the default
|
|
42
|
+
# branch.
|
|
43
|
+
if: github.ref == 'refs/heads/main'
|
|
44
|
+
env:
|
|
45
|
+
# The same path ci.yml uses, so both workflows cache fastembed weights in
|
|
46
|
+
# one place. Set explicitly because fastembed's own default is a
|
|
47
|
+
# subdirectory of the system temp dir -- unkeyable for the cache step
|
|
48
|
+
# below and different on every runner. The test resolves the directory
|
|
49
|
+
# through fastembed's own `define_cache_dir`, so it reads this value
|
|
50
|
+
# rather than assuming it.
|
|
51
|
+
FASTEMBED_CACHE_PATH: /home/runner/.cache/fastembed
|
|
52
|
+
steps:
|
|
53
|
+
- name: Checkout repository
|
|
54
|
+
uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- name: Set up Python
|
|
57
|
+
uses: actions/setup-python@v5
|
|
58
|
+
with:
|
|
59
|
+
python-version: '3.11'
|
|
60
|
+
|
|
61
|
+
- name: Install uv
|
|
62
|
+
uses: astral-sh/setup-uv@v4
|
|
63
|
+
|
|
64
|
+
# Keyed on the model, not on a hash of anything: the weights at a given
|
|
65
|
+
# revision never change, and the job accesses the entry weekly so it does
|
|
66
|
+
# not age out under GitHub's eviction policy.
|
|
67
|
+
- name: Restore the reranker weights
|
|
68
|
+
uses: actions/cache@v4
|
|
69
|
+
with:
|
|
70
|
+
path: ${{ env.FASTEMBED_CACHE_PATH }}
|
|
71
|
+
key: fastembed-bge-reranker-v2-m3
|
|
72
|
+
|
|
73
|
+
- name: Sync dependencies
|
|
74
|
+
run: uv sync --all-extras
|
|
75
|
+
|
|
76
|
+
# Cold cache: this is where the ~2.3 GB download happens, and where a
|
|
77
|
+
# retired repository or a broken ModelSource is caught.
|
|
78
|
+
- name: Fetch the weights if the cache was cold
|
|
79
|
+
run: uv run python -c "from docugraph.retrieval.reranker import FastembedReranker; FastembedReranker()"
|
|
80
|
+
|
|
81
|
+
# `pipefail` so a failing pytest fails the step; `tee` so the log survives
|
|
82
|
+
# the grep. Verbose because the assertion below is on the test's own
|
|
83
|
+
# verdict, and a skip exits 0.
|
|
84
|
+
- name: Run the reranker tests
|
|
85
|
+
run: |
|
|
86
|
+
set -o pipefail
|
|
87
|
+
uv run pytest tests/unit/test_reranker.py -v | tee /tmp/reranker.log
|
|
88
|
+
if ! grep -qE "TestFastembedRerankerLive.*PASSED" /tmp/reranker.log; then
|
|
89
|
+
echo "::error::the live reranker test did not pass -- it skipped, so this run checked nothing"
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|