firecloud-devnet 0.1.0__tar.gz → 0.2.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.
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/.gitignore +4 -3
- firecloud_devnet-0.2.0/CHANGELOG.md +64 -0
- firecloud_devnet-0.2.0/Dockerfile +12 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/PKG-INFO +24 -1
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/README.md +23 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/docker-compose.yml +4 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_mlops/artifact_store.py +10 -1
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_mlops/simulate_failure.py +1 -1
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/cli.py +4 -6
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/config.py +15 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/indexer.py +31 -6
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/query_engine.py +15 -4
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/retriever.py +11 -8
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/cli.py +146 -143
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/crypto.py +4 -32
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/distributor.py +99 -33
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/manifest.py +56 -14
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/node.py +181 -10
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/storage.py +29 -9
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/transport.py +184 -35
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/pyproject.toml +1 -1
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/conftest.py +0 -2
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_chunker.py +4 -4
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_cli.py +0 -3
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_discovery.py +0 -1
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_distributor.py +134 -3
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_integration.py +0 -4
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_manifest.py +88 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_network.py +0 -1
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_node.py +136 -5
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_storage.py +52 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_sync.py +1 -2
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_transport.py +115 -10
- firecloud_devnet-0.1.0/CHANGELOG.md +0 -21
- firecloud_devnet-0.1.0/Dockerfile +0 -18
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/.dockerignore +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/.env.example +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/.github/workflows/ci.yml +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/.github/workflows/publish.yml +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/LICENSE +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_mlops/__init__.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_mlops/__main__.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_mlops/anomaly.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_mlops/cli.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_mlops/telemetry.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/__init__.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/embedder.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/fc_rag/requirements.txt +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/__init__.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/chunker.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/discovery.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/exceptions.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/fec.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/network.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/firecloud/sync.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/__init__.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_crypto.py +0 -0
- {firecloud_devnet-0.1.0 → firecloud_devnet-0.2.0}/tests/test_fec.py +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to FireCloud will be documented in this file.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Erasure-coded files are now downloadable after the cluster shrinks.**
|
|
11
|
+
Download derived the placement strategy from the *live* peer count, so a
|
|
12
|
+
file uploaded with erasure coding became unreadable once fewer than 5
|
|
13
|
+
nodes were online — even with enough shares reachable. The strategy and
|
|
14
|
+
reconstruction threshold now come from the manifest entry.
|
|
15
|
+
- Manifest merges with equal Lamport timestamps now converge on a
|
|
16
|
+
deterministic winner (tombstone first, then uploader node ID) instead of
|
|
17
|
+
each node keeping its own version forever.
|
|
18
|
+
- Chunk stores on peers are now acknowledged; placement metadata
|
|
19
|
+
(`stored_on`) only records nodes that actually persisted the chunk, and
|
|
20
|
+
upload falls back to other nodes when a peer is full.
|
|
21
|
+
- Chunk retrieval responses are matched to requests by chunk ID, so a late
|
|
22
|
+
reply can no longer be delivered to the wrong request; retrieval and
|
|
23
|
+
handshake have timeouts instead of hanging forever on a silent peer.
|
|
24
|
+
- Frame length on the wire is capped (64 MiB) so a corrupt or hostile
|
|
25
|
+
length prefix cannot trigger a multi-gigabyte allocation.
|
|
26
|
+
- Corrupt FEC shares are detected via their integrity hash and skipped
|
|
27
|
+
during reconstruction instead of poisoning the decoded file.
|
|
28
|
+
- Chunk writes are atomic (write-temp-then-rename) and storage usage is
|
|
29
|
+
tracked incrementally instead of re-walking the chunk tree on every store.
|
|
30
|
+
- Manifest and artifact-store JSON files are written atomically.
|
|
31
|
+
- Auth token comparison is constant-time.
|
|
32
|
+
- Re-saving an `fc-ml` artifact with the same name+version replaces the
|
|
33
|
+
record instead of duplicating it.
|
|
34
|
+
- `fc-rag` re-indexing no longer duplicates vector points (deterministic
|
|
35
|
+
point IDs + per-file cleanup), and indexing/retrieval share one embedded
|
|
36
|
+
Qdrant client.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- `firecloud verify [FILE_ID]` — checks chunk availability and integrity
|
|
40
|
+
across the network and reports each file as healthy / degraded /
|
|
41
|
+
unrecoverable (non-zero exit on unrecoverable files).
|
|
42
|
+
- `FIRECLOUD_PASSPHRASE`, `FIRECLOUD_DATA_DIR`, and
|
|
43
|
+
`FIRECLOUD_MAX_STORAGE_GB` environment variables are honored by the CLI
|
|
44
|
+
(docker-compose already set them; the code now reads them).
|
|
45
|
+
- `--bootstrap host:port` option on `start`, `upload`, `download`,
|
|
46
|
+
`delete`, `verify`, and `sync` to connect to a peer without mDNS.
|
|
47
|
+
- Peer-side store/has-chunk protocol messages (`STORE_OK`, `STORE_FAIL`,
|
|
48
|
+
`HAS_CHUNK`) backing the fixes above.
|
|
49
|
+
|
|
50
|
+
## [0.1.0] - 2025-06-01
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- XChaCha20-Poly1305 chunk encryption with HMAC-SHA-256 keyed addressing
|
|
54
|
+
- FastCDC content-defined chunking
|
|
55
|
+
- zfec erasure coding (configurable k/n)
|
|
56
|
+
- mDNS peer discovery via zeroconf with config file fallback
|
|
57
|
+
- TLS binary RPC transport with handshake and heartbeat
|
|
58
|
+
- Manifest with Lamport timestamps and tombstone support
|
|
59
|
+
- Watchdog-based folder sync (outbound upload, inbound download)
|
|
60
|
+
- Click CLI (`firecloud` entry point) — init, start, upload, download, status, peers
|
|
61
|
+
- Docker Compose multi-node setup with health checks
|
|
62
|
+
- GitHub Actions CI (lint → test → build)
|
|
63
|
+
- `fc-rag`: local RAG pipeline (fastembed + Qdrant + Ollama)
|
|
64
|
+
- `fc-ml`: ML artifact versioning, telemetry server, anomaly detection
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firecloud-devnet
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Private, encrypted, distributed storage across your own machines
|
|
5
5
|
Project-URL: Homepage, https://github.com/rajashekharsunkara/firecloud
|
|
6
6
|
Project-URL: Repository, https://github.com/rajashekharsunkara/firecloud
|
|
@@ -87,8 +87,27 @@ docker exec firecloud-bootstrap firecloud upload /data/my-file.zip
|
|
|
87
87
|
|
|
88
88
|
# 3. Download from any node
|
|
89
89
|
docker exec firecloud-node-1 firecloud download <file_id> /data/restored.zip
|
|
90
|
+
|
|
91
|
+
# 4. Check replica/share health across the network
|
|
92
|
+
docker exec firecloud-bootstrap firecloud verify
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### CLI essentials
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
firecloud init # create a network keystore
|
|
99
|
+
firecloud start --bootstrap host:7474 # run a node, optionally joining a peer
|
|
100
|
+
firecloud upload <path> # chunk → encrypt → distribute
|
|
101
|
+
firecloud download <file_id> <output> # retrieve → verify → reassemble
|
|
102
|
+
firecloud verify [file_id] # health report: healthy / degraded / unrecoverable
|
|
103
|
+
firecloud sync <folder> # bi-directional folder sync
|
|
90
104
|
```
|
|
91
105
|
|
|
106
|
+
Environment variables (all optional): `FIRECLOUD_PASSPHRASE` (skip the
|
|
107
|
+
passphrase prompt), `FIRECLOUD_DATA_DIR` (default storage directory),
|
|
108
|
+
`FIRECLOUD_MAX_STORAGE_GB` (chunk-store quota), and `FIRECLOUD_BOOTSTRAP`
|
|
109
|
+
(comma-separated peers to connect to on start).
|
|
110
|
+
|
|
92
111
|
---
|
|
93
112
|
|
|
94
113
|
## Architecture
|
|
@@ -116,6 +135,10 @@ docker exec firecloud-node-1 firecloud download <file_id> /data/restored.zip
|
|
|
116
135
|
|
|
117
136
|
FireCloud uses **HMAC-SHA-256 with a network-derived key** for chunk addressing instead of plain SHA-256. This raises the cost of confirmation-of-file attacks — an attacker who suspects a specific file is stored cannot verify its presence by computing chunk hashes from the plaintext, because valid chunk IDs require the network key. This protection holds as long as the network key remains confidential.
|
|
118
137
|
|
|
138
|
+
Chunks are encrypted with XChaCha20-Poly1305 before leaving the machine, so storage nodes only ever see authenticated ciphertext. Transport hardening includes a frame-size cap, handshake and request timeouts, and constant-time auth-token comparison.
|
|
139
|
+
|
|
140
|
+
**Known limitations (devnet):** node TLS certificates are self-signed and clients do not verify them, so the TLS layer protects against passive snooping but not an active man-in-the-middle on your LAN; the chunk payloads themselves remain end-to-end encrypted regardless. Deploy only on networks you control.
|
|
141
|
+
|
|
119
142
|
---
|
|
120
143
|
|
|
121
144
|
## AI/ML Extensions
|
|
@@ -35,8 +35,27 @@ docker exec firecloud-bootstrap firecloud upload /data/my-file.zip
|
|
|
35
35
|
|
|
36
36
|
# 3. Download from any node
|
|
37
37
|
docker exec firecloud-node-1 firecloud download <file_id> /data/restored.zip
|
|
38
|
+
|
|
39
|
+
# 4. Check replica/share health across the network
|
|
40
|
+
docker exec firecloud-bootstrap firecloud verify
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### CLI essentials
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
firecloud init # create a network keystore
|
|
47
|
+
firecloud start --bootstrap host:7474 # run a node, optionally joining a peer
|
|
48
|
+
firecloud upload <path> # chunk → encrypt → distribute
|
|
49
|
+
firecloud download <file_id> <output> # retrieve → verify → reassemble
|
|
50
|
+
firecloud verify [file_id] # health report: healthy / degraded / unrecoverable
|
|
51
|
+
firecloud sync <folder> # bi-directional folder sync
|
|
38
52
|
```
|
|
39
53
|
|
|
54
|
+
Environment variables (all optional): `FIRECLOUD_PASSPHRASE` (skip the
|
|
55
|
+
passphrase prompt), `FIRECLOUD_DATA_DIR` (default storage directory),
|
|
56
|
+
`FIRECLOUD_MAX_STORAGE_GB` (chunk-store quota), and `FIRECLOUD_BOOTSTRAP`
|
|
57
|
+
(comma-separated peers to connect to on start).
|
|
58
|
+
|
|
40
59
|
---
|
|
41
60
|
|
|
42
61
|
## Architecture
|
|
@@ -64,6 +83,10 @@ docker exec firecloud-node-1 firecloud download <file_id> /data/restored.zip
|
|
|
64
83
|
|
|
65
84
|
FireCloud uses **HMAC-SHA-256 with a network-derived key** for chunk addressing instead of plain SHA-256. This raises the cost of confirmation-of-file attacks — an attacker who suspects a specific file is stored cannot verify its presence by computing chunk hashes from the plaintext, because valid chunk IDs require the network key. This protection holds as long as the network key remains confidential.
|
|
66
85
|
|
|
86
|
+
Chunks are encrypted with XChaCha20-Poly1305 before leaving the machine, so storage nodes only ever see authenticated ciphertext. Transport hardening includes a frame-size cap, handshake and request timeouts, and constant-time auth-token comparison.
|
|
87
|
+
|
|
88
|
+
**Known limitations (devnet):** node TLS certificates are self-signed and clients do not verify them, so the TLS layer protects against passive snooping but not an active man-in-the-middle on your LAN; the chunk payloads themselves remain end-to-end encrypted regardless. Deploy only on networks you control.
|
|
89
|
+
|
|
67
90
|
---
|
|
68
91
|
|
|
69
92
|
## AI/ML Extensions
|
|
@@ -6,6 +6,7 @@ services:
|
|
|
6
6
|
- "7474:7474"
|
|
7
7
|
volumes:
|
|
8
8
|
- fc-bootstrap-data:/data
|
|
9
|
+
- ./test_config/.firecloud:/root/.firecloud:ro,z
|
|
9
10
|
environment:
|
|
10
11
|
- FIRECLOUD_PASSPHRASE=${FIRECLOUD_PASSPHRASE}
|
|
11
12
|
- FIRECLOUD_MAX_STORAGE_GB=${FIRECLOUD_MAX_STORAGE_GB:-10}
|
|
@@ -27,6 +28,7 @@ services:
|
|
|
27
28
|
- "7475:7475"
|
|
28
29
|
volumes:
|
|
29
30
|
- fc-node1-data:/data
|
|
31
|
+
- ./test_config/.firecloud:/root/.firecloud:ro,z
|
|
30
32
|
environment:
|
|
31
33
|
- FIRECLOUD_PASSPHRASE=${FIRECLOUD_PASSPHRASE}
|
|
32
34
|
- FIRECLOUD_BOOTSTRAP=bootstrap-node:7474
|
|
@@ -51,6 +53,7 @@ services:
|
|
|
51
53
|
- "7476:7476"
|
|
52
54
|
volumes:
|
|
53
55
|
- fc-node2-data:/data
|
|
56
|
+
- ./test_config/.firecloud:/root/.firecloud:ro,z
|
|
54
57
|
environment:
|
|
55
58
|
- FIRECLOUD_PASSPHRASE=${FIRECLOUD_PASSPHRASE}
|
|
56
59
|
- FIRECLOUD_BOOTSTRAP=bootstrap-node:7474
|
|
@@ -75,6 +78,7 @@ services:
|
|
|
75
78
|
- "7477:7477"
|
|
76
79
|
volumes:
|
|
77
80
|
- fc-node3-data:/data
|
|
81
|
+
- ./test_config/.firecloud:/root/.firecloud:ro,z
|
|
78
82
|
environment:
|
|
79
83
|
- FIRECLOUD_PASSPHRASE=${FIRECLOUD_PASSPHRASE}
|
|
80
84
|
- FIRECLOUD_BOOTSTRAP=bootstrap-node:7474
|
|
@@ -36,10 +36,13 @@ def _load_manifest() -> list[dict]:
|
|
|
36
36
|
|
|
37
37
|
def _save_manifest(entries: list[dict]) -> None:
|
|
38
38
|
_MANIFEST_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
|
|
39
|
+
# Write-then-rename so a crash mid-write cannot truncate the manifest.
|
|
40
|
+
tmp_path = _MANIFEST_PATH.with_name(_MANIFEST_PATH.name + ".tmp")
|
|
41
|
+
tmp_path.write_text(
|
|
40
42
|
json.dumps(entries, indent=2, default=str),
|
|
41
43
|
encoding="utf-8",
|
|
42
44
|
)
|
|
45
|
+
tmp_path.replace(_MANIFEST_PATH)
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
async def save_artifact(
|
|
@@ -70,6 +73,12 @@ async def save_artifact(
|
|
|
70
73
|
)
|
|
71
74
|
|
|
72
75
|
entries = _load_manifest()
|
|
76
|
+
# Re-saving the same name+version replaces the old record — otherwise
|
|
77
|
+
# load_artifact would keep returning the stale first match forever.
|
|
78
|
+
entries = [
|
|
79
|
+
e for e in entries
|
|
80
|
+
if not (e.get("name") == name and e.get("version") == version)
|
|
81
|
+
]
|
|
73
82
|
entries.append(metadata.model_dump())
|
|
74
83
|
_save_manifest(entries)
|
|
75
84
|
return metadata
|
|
@@ -36,14 +36,12 @@ def index(path: str):
|
|
|
36
36
|
def query(question: str):
|
|
37
37
|
"""Query the local RAG pipeline with a natural-language question."""
|
|
38
38
|
from fc_rag.query_engine import query as run_query
|
|
39
|
-
from fc_rag.retriever import retrieve
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
click.echo(answer)
|
|
40
|
+
response = run_query(question)
|
|
41
|
+
click.echo(response.answer)
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sources = sorted(set(r.filename for r in results))
|
|
43
|
+
if response.sources:
|
|
44
|
+
sources = sorted(set(r.filename for r in response.sources))
|
|
47
45
|
click.echo(f"\nSources: {', '.join(sources)}")
|
|
48
46
|
|
|
49
47
|
|
|
@@ -22,3 +22,18 @@ class Settings(BaseModel):
|
|
|
22
22
|
@lru_cache(maxsize=1)
|
|
23
23
|
def get_settings() -> Settings:
|
|
24
24
|
return Settings()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@lru_cache(maxsize=1)
|
|
28
|
+
def get_client():
|
|
29
|
+
"""Shared embedded-Qdrant client.
|
|
30
|
+
|
|
31
|
+
Embedded Qdrant locks its storage directory per client instance, so
|
|
32
|
+
indexing and retrieval within one process must reuse a single client
|
|
33
|
+
rather than each opening their own.
|
|
34
|
+
"""
|
|
35
|
+
from qdrant_client import QdrantClient
|
|
36
|
+
|
|
37
|
+
settings = get_settings()
|
|
38
|
+
settings.qdrant_path.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return QdrantClient(path=str(settings.qdrant_path))
|
|
@@ -5,10 +5,18 @@ from datetime import datetime, timezone
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from qdrant_client import QdrantClient
|
|
8
|
-
from qdrant_client.models import
|
|
8
|
+
from qdrant_client.models import (
|
|
9
|
+
Distance,
|
|
10
|
+
FieldCondition,
|
|
11
|
+
Filter,
|
|
12
|
+
FilterSelector,
|
|
13
|
+
MatchValue,
|
|
14
|
+
PointStruct,
|
|
15
|
+
VectorParams,
|
|
16
|
+
)
|
|
9
17
|
from rich.progress import Progress
|
|
10
18
|
|
|
11
|
-
from fc_rag.config import get_settings
|
|
19
|
+
from fc_rag.config import get_client, get_settings
|
|
12
20
|
from fc_rag.embedder import chunk_text, embed_chunks
|
|
13
21
|
|
|
14
22
|
_SUPPORTED_EXTENSIONS = {".txt", ".md", ".py", ".json"}
|
|
@@ -64,9 +72,7 @@ def index_path(path: Path) -> int:
|
|
|
64
72
|
_safety_check(path)
|
|
65
73
|
|
|
66
74
|
settings = get_settings()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
client = QdrantClient(path=str(settings.qdrant_path))
|
|
75
|
+
client = get_client()
|
|
70
76
|
_ensure_collection(client, settings.collection_name)
|
|
71
77
|
|
|
72
78
|
if path.is_file():
|
|
@@ -99,9 +105,28 @@ def index_path(path: Path) -> int:
|
|
|
99
105
|
vectors = embed_chunks(chunks)
|
|
100
106
|
now = datetime.now(timezone.utc).isoformat()
|
|
101
107
|
|
|
108
|
+
# Drop previously indexed points for this file so re-indexing
|
|
109
|
+
# cannot accumulate duplicates or leave stale trailing chunks.
|
|
110
|
+
client.delete(
|
|
111
|
+
collection_name=settings.collection_name,
|
|
112
|
+
points_selector=FilterSelector(
|
|
113
|
+
filter=Filter(
|
|
114
|
+
must=[
|
|
115
|
+
FieldCondition(
|
|
116
|
+
key="filepath",
|
|
117
|
+
match=MatchValue(value=str(filepath)),
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
102
124
|
points = [
|
|
103
125
|
PointStruct(
|
|
104
|
-
|
|
126
|
+
# Deterministic ID per (file, chunk position) — an
|
|
127
|
+
# upsert of the same file overwrites instead of
|
|
128
|
+
# duplicating.
|
|
129
|
+
id=str(uuid.uuid5(uuid.NAMESPACE_URL, f"{filepath}::{i}")),
|
|
105
130
|
vector=vec,
|
|
106
131
|
payload={
|
|
107
132
|
"filename": filepath.name,
|
|
@@ -6,12 +6,23 @@ import time
|
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
|
|
8
8
|
import ollama
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
9
11
|
from fc_rag.config import get_settings
|
|
10
|
-
from fc_rag.retriever import retrieve
|
|
12
|
+
from fc_rag.retriever import RetrievalResult, retrieve
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueryResponse(BaseModel):
|
|
16
|
+
"""Answer plus the retrieved chunks it was grounded on."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(frozen=True)
|
|
19
|
+
|
|
20
|
+
answer: str
|
|
21
|
+
sources: list[RetrievalResult]
|
|
11
22
|
|
|
12
23
|
|
|
13
|
-
def query(user_question: str) ->
|
|
14
|
-
"""Run the full RAG pipeline and return the
|
|
24
|
+
def query(user_question: str) -> QueryResponse:
|
|
25
|
+
"""Run the full RAG pipeline and return the answer with its sources."""
|
|
15
26
|
settings = get_settings()
|
|
16
27
|
start = time.monotonic()
|
|
17
28
|
|
|
@@ -76,4 +87,4 @@ def query(user_question: str) -> str:
|
|
|
76
87
|
"success": success,
|
|
77
88
|
}, default=str) + "\n")
|
|
78
89
|
|
|
79
|
-
return answer
|
|
90
|
+
return QueryResponse(answer=answer, sources=results)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""Search the local Qdrant collection for relevant chunks."""
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict
|
|
4
|
-
from qdrant_client import QdrantClient
|
|
5
4
|
|
|
6
|
-
from fc_rag.config import get_settings
|
|
5
|
+
from fc_rag.config import get_client, get_settings
|
|
7
6
|
from fc_rag.embedder import embed_chunks
|
|
8
7
|
|
|
9
8
|
|
|
@@ -26,13 +25,17 @@ def retrieve(query: str, top_k: int | None = None) -> list[RetrievalResult]:
|
|
|
26
25
|
return []
|
|
27
26
|
query_vector = vectors[0]
|
|
28
27
|
|
|
29
|
-
client =
|
|
28
|
+
client = get_client()
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
try:
|
|
31
|
+
response = client.query_points(
|
|
32
|
+
collection_name=settings.collection_name,
|
|
33
|
+
query=query_vector,
|
|
34
|
+
limit=k,
|
|
35
|
+
)
|
|
36
|
+
except Exception:
|
|
37
|
+
# Collection does not exist yet — nothing has been indexed.
|
|
38
|
+
return []
|
|
36
39
|
results = response.points
|
|
37
40
|
|
|
38
41
|
return [
|