Skip to content

Commit 243c5ca

Browse files
committed
feat(api): Enhance configuration management and indexing capabilities
- Introduced a caching mechanism for configuration loading to improve performance and reliability. - Implemented endpoints for managing configuration sections, including retrieval, updates, and resets. - Added functionality for indexing repositories, including status tracking and statistics retrieval. - Enhanced the chunking and embedding processes with improved error handling and language detection. - Updated the dashboard API to reflect changes in terminology and structure, aligning with the new TriBrid configuration. - Added new endpoints for reranking and triplet management, with placeholders for future implementations. This commit significantly improves the API's configuration handling and indexing features, setting the stage for more robust data processing and retrieval capabilities.
1 parent b079957 commit 243c5ca

32 files changed

Lines changed: 2226 additions & 5649 deletions
179 KB
Loading

server/api/config.py

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,96 @@
1-
from fastapi import APIRouter
1+
from __future__ import annotations
2+
3+
import os
4+
5+
from fastapi import APIRouter, HTTPException, Query
26
from typing import Any
37

8+
from server.config import load_config, save_config
49
from server.models.tribrid_config_model import TriBridConfig
510

611
router = APIRouter(tags=["config"])
712

8-
# In-memory config store
9-
_config: TriBridConfig | None = None
13+
# In-memory cache (source of truth is tribrid_config.json)
14+
_config_cache: TriBridConfig | None = None
1015

1116

1217
def _get_default_config() -> TriBridConfig:
1318
"""Get default config - LAW provides all defaults via default_factory."""
1419
return TriBridConfig()
1520

1621

22+
def _load_or_init_config() -> TriBridConfig:
23+
"""Load config from disk; initialize defaults if missing/invalid."""
24+
global _config_cache
25+
if _config_cache is not None:
26+
return _config_cache
27+
try:
28+
_config_cache = load_config()
29+
return _config_cache
30+
except FileNotFoundError:
31+
_config_cache = _get_default_config()
32+
save_config(_config_cache)
33+
return _config_cache
34+
except Exception as e:
35+
# Surface validation errors clearly (Pydantic will raise)
36+
raise HTTPException(status_code=500, detail=f"Failed to load tribrid_config.json: {e}")
37+
38+
1739
@router.get("/config", response_model=TriBridConfig)
1840
async def get_config() -> TriBridConfig:
19-
global _config
20-
if _config is None:
21-
_config = _get_default_config()
22-
return _config
41+
return _load_or_init_config()
2342

2443

2544
@router.put("/config", response_model=TriBridConfig)
2645
async def update_config(config: TriBridConfig) -> TriBridConfig:
27-
raise NotImplementedError
46+
global _config_cache
47+
# Persist full config to disk
48+
save_config(config)
49+
_config_cache = config
50+
return config
2851

2952

3053
@router.patch("/config/{section}", response_model=TriBridConfig)
3154
async def update_config_section(section: str, updates: dict[str, Any]) -> TriBridConfig:
32-
raise NotImplementedError
55+
global _config_cache
56+
config = _load_or_init_config()
57+
58+
# Only allow patching known top-level sections
59+
if section not in TriBridConfig.model_fields:
60+
raise HTTPException(status_code=404, detail=f"Unknown config section: {section}")
61+
62+
# Build a new config dict with patched section and re-validate (ensures Field constraints apply)
63+
base = config.model_dump()
64+
current_section = base.get(section)
65+
if not isinstance(current_section, dict):
66+
raise HTTPException(status_code=400, detail=f"Config section '{section}' is not patchable")
67+
if not isinstance(updates, dict):
68+
raise HTTPException(status_code=422, detail="PATCH body must be a JSON object")
69+
70+
merged = {**current_section, **updates}
71+
base[section] = merged
72+
73+
try:
74+
new_config = TriBridConfig.model_validate(base)
75+
except Exception as e:
76+
raise HTTPException(status_code=422, detail=str(e))
77+
78+
save_config(new_config)
79+
_config_cache = new_config
80+
return new_config
3381

3482

3583
@router.post("/config/reset", response_model=TriBridConfig)
3684
async def reset_config() -> TriBridConfig:
37-
raise NotImplementedError
85+
global _config_cache
86+
cfg = _get_default_config()
87+
save_config(cfg)
88+
_config_cache = cfg
89+
return cfg
90+
91+
92+
@router.get("/secrets/check")
93+
async def check_secrets(keys: str = Query(..., description="Comma-separated env var names")) -> dict[str, bool]:
94+
"""Return which secret env vars are configured (never returns values)."""
95+
names = [k.strip() for k in (keys or "").split(",") if k.strip()]
96+
return {name: bool(os.getenv(name)) for name in names}

server/api/index.py

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,166 @@
1-
from fastapi import APIRouter
1+
from __future__ import annotations
2+
3+
from collections import defaultdict
4+
from datetime import datetime, timezone
25
from typing import Any
36

7+
from fastapi import APIRouter, HTTPException
8+
9+
from server.config import load_config
10+
from server.db.postgres import PostgresClient
11+
from server.indexing.chunker import Chunker
12+
from server.indexing.embedder import Embedder
13+
from server.indexing.loader import FileLoader
414
from server.models.index import IndexRequest, IndexStats, IndexStatus
515

616
router = APIRouter(tags=["index"])
717

18+
_STATUS: dict[str, IndexStatus] = {}
19+
_STATS: dict[str, IndexStats] = {}
20+
21+
22+
async def _run_index(repo_id: str, repo_path: str, force_reindex: bool) -> IndexStats:
23+
cfg = load_config()
24+
25+
if not force_reindex and repo_id in _STATS:
26+
return _STATS[repo_id]
27+
28+
# Build ignore patterns from config
29+
ignore_patterns: list[str] = []
30+
exts = (cfg.indexing.index_excluded_exts or "").split(",")
31+
for ext in exts:
32+
ext = ext.strip()
33+
if not ext:
34+
continue
35+
if not ext.startswith("."):
36+
ext = "." + ext
37+
ignore_patterns.append(f"*{ext}")
38+
39+
loader = FileLoader(ignore_patterns=ignore_patterns)
40+
chunker = Chunker(cfg.chunking)
41+
embedder = Embedder(cfg.embedding)
42+
postgres = PostgresClient(cfg.indexing.postgres_url)
43+
44+
total_files = 0
45+
total_chunks = 0
46+
total_tokens = 0
47+
file_breakdown: dict[str, int] = defaultdict(int)
48+
49+
prev_status = _STATUS.get(repo_id)
50+
started_at = prev_status.started_at if prev_status and prev_status.started_at else datetime.now(timezone.utc)
51+
52+
all_chunks = []
53+
for rel_path, content in loader.load_repo(repo_path):
54+
total_files += 1
55+
ext = "." + rel_path.split(".")[-1] if "." in rel_path else ""
56+
file_breakdown[ext] += 1
57+
58+
_STATUS[repo_id] = IndexStatus(
59+
repo_id=repo_id,
60+
status="indexing",
61+
progress=0.0,
62+
current_file=rel_path,
63+
started_at=started_at,
64+
)
65+
66+
chunks = chunker.chunk_file(rel_path, content)
67+
total_chunks += len(chunks)
68+
total_tokens += sum(int(c.token_count or 0) for c in chunks)
69+
all_chunks.extend(chunks)
70+
71+
# Embed + store
72+
embedded = await embedder.embed_chunks(all_chunks)
73+
await postgres.upsert_embeddings(repo_id, embedded)
74+
await postgres.upsert_fts(repo_id, embedded)
75+
76+
# Stash embedding model for stats consumers
77+
PostgresClient._STORE.setdefault(repo_id, {})["embedding_model"] = cfg.embedding.embedding_model
78+
79+
stats = IndexStats(
80+
repo_id=repo_id,
81+
total_files=total_files,
82+
total_chunks=total_chunks,
83+
total_tokens=total_tokens,
84+
embedding_model=cfg.embedding.embedding_model,
85+
embedding_dimensions=embedder.dim,
86+
last_indexed=datetime.now(timezone.utc),
87+
file_breakdown=dict(file_breakdown),
88+
)
89+
_STATS[repo_id] = stats
90+
return stats
91+
892

993
@router.post("/index", response_model=IndexStatus)
1094
async def start_index(request: IndexRequest) -> IndexStatus:
11-
raise NotImplementedError
95+
started_at = datetime.now(timezone.utc)
96+
_STATUS[request.repo_id] = IndexStatus(
97+
repo_id=request.repo_id,
98+
status="indexing",
99+
progress=0.0,
100+
current_file=None,
101+
started_at=started_at,
102+
)
103+
104+
try:
105+
await _run_index(request.repo_id, request.repo_path, request.force_reindex)
106+
except Exception as e:
107+
prev = _STATUS.get(request.repo_id)
108+
_STATUS[request.repo_id] = IndexStatus(
109+
repo_id=request.repo_id,
110+
status="error",
111+
progress=0.0,
112+
current_file=prev.current_file if prev else None,
113+
error=str(e),
114+
started_at=started_at,
115+
completed_at=datetime.now(timezone.utc),
116+
)
117+
raise HTTPException(status_code=500, detail=str(e))
118+
119+
_STATUS[request.repo_id] = IndexStatus(
120+
repo_id=request.repo_id,
121+
status="complete",
122+
progress=1.0,
123+
current_file=None,
124+
started_at=started_at,
125+
completed_at=datetime.now(timezone.utc),
126+
)
127+
return _STATUS[request.repo_id]
12128

13129

14130
@router.get("/index/{repo_id}/status", response_model=IndexStatus)
15131
async def get_index_status(repo_id: str) -> IndexStatus:
16-
raise NotImplementedError
132+
if repo_id in _STATUS:
133+
return _STATUS[repo_id]
134+
return IndexStatus(
135+
repo_id=repo_id,
136+
status="idle",
137+
progress=0.0,
138+
current_file=None,
139+
error=None,
140+
started_at=None,
141+
completed_at=None,
142+
)
17143

18144

19145
@router.get("/index/{repo_id}/stats", response_model=IndexStats)
20146
async def get_index_stats(repo_id: str) -> IndexStats:
21-
raise NotImplementedError
147+
if repo_id in _STATS:
148+
return _STATS[repo_id]
149+
# Try to read from in-memory store (if indexed by another process)
150+
cfg = load_config()
151+
postgres = PostgresClient(cfg.indexing.postgres_url)
152+
stats = await postgres.get_index_stats(repo_id)
153+
if stats.total_chunks == 0:
154+
raise HTTPException(status_code=404, detail=f"No index found for repo_id={repo_id}")
155+
return stats
22156

23157

24158
@router.delete("/index/{repo_id}")
25159
async def delete_index(repo_id: str) -> dict[str, Any]:
26-
raise NotImplementedError
160+
cfg = load_config()
161+
postgres = PostgresClient(cfg.indexing.postgres_url)
162+
deleted_vec = await postgres.delete_embeddings(repo_id)
163+
deleted_fts = await postgres.delete_fts(repo_id)
164+
_STATUS.pop(repo_id, None)
165+
_STATS.pop(repo_id, None)
166+
return {"ok": True, "deleted_embeddings": deleted_vec, "deleted_fts": deleted_fts}

0 commit comments

Comments
 (0)