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.
Files changed (96) hide show
  1. docugraph-0.1.0/.dockerignore +63 -0
  2. docugraph-0.1.0/.github/dependabot.yml +101 -0
  3. docugraph-0.1.0/.github/workflows/ci.yml +210 -0
  4. docugraph-0.1.0/.github/workflows/docker-publish.yml +85 -0
  5. docugraph-0.1.0/.github/workflows/live-provider-smoke.yml +77 -0
  6. docugraph-0.1.0/.github/workflows/live-reranker.yml +91 -0
  7. docugraph-0.1.0/.github/workflows/release.yml +124 -0
  8. docugraph-0.1.0/.gitignore +13 -0
  9. docugraph-0.1.0/.pre-commit-config.yaml +62 -0
  10. docugraph-0.1.0/.python-version +1 -0
  11. docugraph-0.1.0/LICENSE +201 -0
  12. docugraph-0.1.0/NOTICE +10 -0
  13. docugraph-0.1.0/PKG-INFO +354 -0
  14. docugraph-0.1.0/README.md +296 -0
  15. docugraph-0.1.0/claude-integration/README.md +314 -0
  16. docugraph-0.1.0/claude-integration/hooks/docugraph-auto-index.sh +79 -0
  17. docugraph-0.1.0/claude-integration/hooks/docugraph-auto-search.sh +56 -0
  18. docugraph-0.1.0/claude-integration/hooks/docugraph-post-compact.sh +151 -0
  19. docugraph-0.1.0/claude-integration/hooks/docugraph-pre-compact.sh +157 -0
  20. docugraph-0.1.0/claude-integration/hooks/docugraph-rule-check.sh +93 -0
  21. docugraph-0.1.0/claude-integration/hooks/docugraph-session-end.sh +86 -0
  22. docugraph-0.1.0/claude-integration/hooks/docugraph-session-start.sh +44 -0
  23. docugraph-0.1.0/claude-integration/install.sh +113 -0
  24. docugraph-0.1.0/claude-integration/settings.json +88 -0
  25. docugraph-0.1.0/claude-integration/skills/architecture/SKILL.md +138 -0
  26. docugraph-0.1.0/claude-integration/skills/context/SKILL.md +120 -0
  27. docugraph-0.1.0/claude-integration/skills/docs/SKILL.md +70 -0
  28. docugraph-0.1.0/claude-integration/skills/index/SKILL.md +65 -0
  29. docugraph-0.1.0/claude-integration/skills/recall/SKILL.md +76 -0
  30. docugraph-0.1.0/claude-integration/skills/remember/SKILL.md +68 -0
  31. docugraph-0.1.0/docker/Dockerfile +111 -0
  32. docugraph-0.1.0/docker/docker-compose.yml +46 -0
  33. docugraph-0.1.0/docker-compose.yml +22 -0
  34. docugraph-0.1.0/docs/SECURITY.md +45 -0
  35. docugraph-0.1.0/docs/configuration.md +316 -0
  36. docugraph-0.1.0/docs/quickstart.md +262 -0
  37. docugraph-0.1.0/examples/crawl_and_search.py +118 -0
  38. docugraph-0.1.0/examples/index_local_repo.py +114 -0
  39. docugraph-0.1.0/examples/mcp_client_example.py +130 -0
  40. docugraph-0.1.0/pyproject.toml +223 -0
  41. docugraph-0.1.0/scripts/live_provider_smoke.py +138 -0
  42. docugraph-0.1.0/src/docugraph/__init__.py +24 -0
  43. docugraph-0.1.0/src/docugraph/_version.py +28 -0
  44. docugraph-0.1.0/src/docugraph/agents/__init__.py +27 -0
  45. docugraph-0.1.0/src/docugraph/agents/coding_agent.py +532 -0
  46. docugraph-0.1.0/src/docugraph/agents/tools/__init__.py +62 -0
  47. docugraph-0.1.0/src/docugraph/agents/tools/graph_query.py +203 -0
  48. docugraph-0.1.0/src/docugraph/agents/tools/memory_ops.py +305 -0
  49. docugraph-0.1.0/src/docugraph/agents/tools/search_code.py +305 -0
  50. docugraph-0.1.0/src/docugraph/agents/tools/search_docs.py +148 -0
  51. docugraph-0.1.0/src/docugraph/core/__init__.py +34 -0
  52. docugraph-0.1.0/src/docugraph/core/config.py +183 -0
  53. docugraph-0.1.0/src/docugraph/core/embeddings.py +394 -0
  54. docugraph-0.1.0/src/docugraph/core/llm.py +892 -0
  55. docugraph-0.1.0/src/docugraph/core/models.py +145 -0
  56. docugraph-0.1.0/src/docugraph/ingestion/__init__.py +64 -0
  57. docugraph-0.1.0/src/docugraph/ingestion/api_docs.py +646 -0
  58. docugraph-0.1.0/src/docugraph/ingestion/chunker.py +382 -0
  59. docugraph-0.1.0/src/docugraph/ingestion/crawler.py +305 -0
  60. docugraph-0.1.0/src/docugraph/ingestion/git_indexer.py +556 -0
  61. docugraph-0.1.0/src/docugraph/ingestion/local_files.py +516 -0
  62. docugraph-0.1.0/src/docugraph/interfaces/__init__.py +1 -0
  63. docugraph-0.1.0/src/docugraph/interfaces/cli.py +983 -0
  64. docugraph-0.1.0/src/docugraph/interfaces/mcp_server.py +918 -0
  65. docugraph-0.1.0/src/docugraph/retrieval/__init__.py +54 -0
  66. docugraph-0.1.0/src/docugraph/retrieval/context_builder.py +522 -0
  67. docugraph-0.1.0/src/docugraph/retrieval/hybrid_search.py +534 -0
  68. docugraph-0.1.0/src/docugraph/retrieval/reranker.py +532 -0
  69. docugraph-0.1.0/src/docugraph/storage/__init__.py +14 -0
  70. docugraph-0.1.0/src/docugraph/storage/graph_store.py +602 -0
  71. docugraph-0.1.0/src/docugraph/storage/memory_store.py +529 -0
  72. docugraph-0.1.0/src/docugraph/storage/vector_store.py +367 -0
  73. docugraph-0.1.0/tests/e2e/__init__.py +1 -0
  74. docugraph-0.1.0/tests/e2e/test_cli.py +159 -0
  75. docugraph-0.1.0/tests/e2e/test_hooks.py +139 -0
  76. docugraph-0.1.0/tests/e2e/test_mcp_server.py +189 -0
  77. docugraph-0.1.0/tests/e2e/tools_snapshot.json +262 -0
  78. docugraph-0.1.0/tests/integration/__init__.py +1 -0
  79. docugraph-0.1.0/tests/integration/test_graph_store_init.py +137 -0
  80. docugraph-0.1.0/tests/integration/test_memory_store.py +152 -0
  81. docugraph-0.1.0/tests/unit/__init__.py +1 -0
  82. docugraph-0.1.0/tests/unit/test_chunker.py +180 -0
  83. docugraph-0.1.0/tests/unit/test_cloud_provider_sdks.py +372 -0
  84. docugraph-0.1.0/tests/unit/test_docs_consistency.py +441 -0
  85. docugraph-0.1.0/tests/unit/test_embeddings.py +185 -0
  86. docugraph-0.1.0/tests/unit/test_event_loop_acquisition.py +376 -0
  87. docugraph-0.1.0/tests/unit/test_graph_embedder_adapter.py +66 -0
  88. docugraph-0.1.0/tests/unit/test_graphiti_llm_client.py +75 -0
  89. docugraph-0.1.0/tests/unit/test_hybrid_graph_status.py +215 -0
  90. docugraph-0.1.0/tests/unit/test_llm_preflight.py +355 -0
  91. docugraph-0.1.0/tests/unit/test_models.py +306 -0
  92. docugraph-0.1.0/tests/unit/test_no_model_ids_in_source.py +205 -0
  93. docugraph-0.1.0/tests/unit/test_reranker.py +185 -0
  94. docugraph-0.1.0/tests/unit/test_vector_store.py +94 -0
  95. docugraph-0.1.0/tests/unit/test_version.py +67 -0
  96. 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