sofhiaazzhr commited on
Commit
efc0c0a
·
1 Parent(s): 8557a20

initial structure

Browse files
Files changed (44) hide show
  1. scripts/build_initial_catalogs.py +16 -0
  2. scripts/enrich_all_sources.py +16 -0
  3. src/agents/intent_router.py +24 -0
  4. src/catalog/enricher.py +16 -0
  5. src/catalog/introspect/base.py +17 -0
  6. src/catalog/introspect/database.py +16 -0
  7. src/catalog/introspect/tabular.py +15 -0
  8. src/catalog/models.py +55 -0
  9. src/catalog/pii_detector.py +16 -0
  10. src/catalog/reader.py +23 -0
  11. src/catalog/store.py +20 -0
  12. src/catalog/validator.py +24 -0
  13. src/config/prompts/__init__.py +0 -0
  14. src/config/prompts/catalog_enricher.md +21 -0
  15. src/config/prompts/chatbot_system.md +17 -0
  16. src/config/prompts/guardrails.md +12 -0
  17. src/config/prompts/intent_router.md +25 -0
  18. src/config/prompts/query_planner.md +25 -0
  19. src/models/api/__init__.py +1 -0
  20. src/models/api/catalog.py +14 -0
  21. src/models/api/chat.py +17 -0
  22. src/models/api/document.py +9 -0
  23. src/pipeline/document_pipeline.py +11 -0
  24. src/pipeline/orchestrator.py +11 -0
  25. src/pipeline/structured_pipeline.py +13 -0
  26. src/pipeline/triggers.py +21 -0
  27. src/query/compiler/base.py +13 -0
  28. src/query/compiler/pandas.py +22 -0
  29. src/query/compiler/sql.py +28 -0
  30. src/query/executor/base.py +26 -0
  31. src/query/executor/db.py +25 -0
  32. src/query/executor/dispatcher.py +17 -0
  33. src/query/executor/tabular.py +24 -0
  34. src/query/ir/models.py +60 -0
  35. src/query/ir/operators.py +17 -0
  36. src/query/ir/validator.py +28 -0
  37. src/query/planner/prompt.py +12 -0
  38. src/query/planner/service.py +15 -0
  39. src/query/service.py +15 -0
  40. src/retrieval/document.py +15 -0
  41. src/retrieval/router.py +11 -0
  42. src/security/auth.py +21 -0
  43. src/security/credentials.py +15 -0
  44. src/security/pii_patterns.py +20 -0
scripts/build_initial_catalogs.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backfill catalogs for existing users.
2
+
3
+ One-off script. For each user that already has registered DB connections or
4
+ uploaded tabular files, run the structured pipeline to build their catalog.
5
+
6
+ Usage:
7
+ uv run python scripts/build_initial_catalogs.py [--user-id USER_ID]
8
+ """
9
+
10
+
11
+ def main() -> None:
12
+ raise NotImplementedError
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
scripts/enrich_all_sources.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bulk re-run CatalogEnricher with the current prompt.
2
+
3
+ For when src/config/prompts/catalog_enricher.md changes and existing
4
+ catalog descriptions need to be regenerated across all users.
5
+
6
+ Usage:
7
+ uv run python scripts/enrich_all_sources.py [--user-id USER_ID]
8
+ """
9
+
10
+
11
+ def main() -> None:
12
+ raise NotImplementedError
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
src/agents/intent_router.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IntentRouter — classifies a user message and emits source_hint.
2
+
3
+ Output: needs_search (bool) + source_hint ∈ { chat, unstructured, structured }.
4
+
5
+ Replaces the previous orchestration.py once the chat endpoint is rewired.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Literal
10
+
11
+
12
+ SourceHint = Literal["chat", "unstructured", "structured"]
13
+
14
+
15
+ @dataclass
16
+ class IntentRouterDecision:
17
+ needs_search: bool
18
+ source_hint: SourceHint
19
+ rewritten_query: str | None = None
20
+
21
+
22
+ class IntentRouter:
23
+ async def classify(self, message: str) -> IntentRouterDecision:
24
+ raise NotImplementedError
src/catalog/enricher.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CatalogEnricher — runs 1 LLM call per source to generate AI descriptions.
2
+
3
+ Input: a draft Source produced by an introspector (raw schema, no descriptions).
4
+ Output: the same Source enriched with description fields at source/table/column level.
5
+
6
+ Prompt: src/config/prompts/catalog_enricher.md
7
+ """
8
+
9
+ from .models import Source
10
+
11
+
12
+ class CatalogEnricher:
13
+ """Adds AI-generated descriptions to a freshly introspected source."""
14
+
15
+ async def enrich(self, source: Source) -> Source:
16
+ raise NotImplementedError
src/catalog/introspect/base.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BaseIntrospector — contract for source-specific schema readers.
2
+
3
+ Subclasses produce a draft Source object with raw schema (names + types +
4
+ sample values). The CatalogEnricher then adds descriptions in a separate step.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+
9
+ from ..models import Source
10
+
11
+
12
+ class BaseIntrospector(ABC):
13
+ """Abstract base. Subclasses: DatabaseIntrospector, TabularIntrospector."""
14
+
15
+ @abstractmethod
16
+ async def introspect(self, location_ref: str) -> Source:
17
+ ...
src/catalog/introspect/database.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Database schema introspection (Postgres / MySQL / Supabase).
2
+
3
+ Reads information_schema for tables/columns/types, samples ~100 rows per table
4
+ for `sample_values` and basic stats. Does NOT generate descriptions
5
+ (that happens in CatalogEnricher).
6
+ """
7
+
8
+ from ..models import Source
9
+ from .base import BaseIntrospector
10
+
11
+
12
+ class DatabaseIntrospector(BaseIntrospector):
13
+ """Connect to user DB → read information_schema → sample 100 rows/table."""
14
+
15
+ async def introspect(self, location_ref: str) -> Source:
16
+ raise NotImplementedError
src/catalog/introspect/tabular.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tabular file schema introspection (Parquet / CSV / XLSX).
2
+
3
+ Reads file headers + samples ~100 rows. For XLSX, each sheet becomes a Table.
4
+ Files are expected to live in Azure Blob (location_ref like az_blob://...).
5
+ """
6
+
7
+ from ..models import Source
8
+ from .base import BaseIntrospector
9
+
10
+
11
+ class TabularIntrospector(BaseIntrospector):
12
+ """Read column names, dtypes, and sample values from Parquet/CSV/XLSX."""
13
+
14
+ async def introspect(self, location_ref: str) -> Source:
15
+ raise NotImplementedError
src/catalog/models.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pydantic models for the per-user data catalog (Cs + Ct).
2
+
3
+ See ARCHITECTURE.md §6 for the full schema definition.
4
+ """
5
+
6
+ from datetime import datetime
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ SourceType = Literal["schema", "tabular", "unstructured"]
13
+ DataType = Literal["int", "decimal", "string", "datetime", "date", "bool", "json"]
14
+
15
+
16
+ class ColumnStats(BaseModel):
17
+ min: Any | None = None
18
+ max: Any | None = None
19
+ distinct_count: int | None = None
20
+
21
+
22
+ class Column(BaseModel):
23
+ column_id: str
24
+ name: str
25
+ data_type: DataType
26
+ description: str
27
+ nullable: bool
28
+ pii_flag: bool = False
29
+ sample_values: list[Any] | None = None
30
+ stats: ColumnStats | None = None
31
+
32
+
33
+ class Table(BaseModel):
34
+ table_id: str
35
+ name: str
36
+ description: str
37
+ row_count: int | None = None
38
+ columns: list[Column]
39
+
40
+
41
+ class Source(BaseModel):
42
+ source_id: str
43
+ source_type: SourceType
44
+ name: str
45
+ description: str
46
+ location_ref: str
47
+ updated_at: datetime
48
+ tables: list[Table] = Field(default_factory=list)
49
+
50
+
51
+ class Catalog(BaseModel):
52
+ user_id: str
53
+ schema_version: str = "1.0"
54
+ generated_at: datetime
55
+ sources: list[Source] = Field(default_factory=list)
src/catalog/pii_detector.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PII auto-detection for catalog columns.
2
+
3
+ When pii_flag is set True, sample_values is forced to None so real PII
4
+ never enters LLM prompts.
5
+
6
+ Patterns live in src/security/pii_patterns.py.
7
+ """
8
+
9
+ from .models import Column
10
+
11
+
12
+ class PIIDetector:
13
+ """Marks columns as pii_flag=True when name/values look sensitive."""
14
+
15
+ def detect(self, column: Column) -> bool:
16
+ raise NotImplementedError
src/catalog/reader.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CatalogReader — loads + filters catalog by source_hint.
2
+
3
+ For typical users (≤50 tables), returns the FULL catalog with no slicing.
4
+ Catalog-level search is added later if catalog grows past the limit.
5
+ """
6
+
7
+ from typing import Literal
8
+
9
+ from .models import Catalog
10
+ from .store import CatalogStore
11
+
12
+
13
+ SourceHint = Literal["chat", "unstructured", "structured"]
14
+
15
+
16
+ class CatalogReader:
17
+ """Loads the user's catalog and filters by source_hint."""
18
+
19
+ def __init__(self, store: CatalogStore) -> None:
20
+ self._store = store
21
+
22
+ async def read(self, user_id: str, source_hint: SourceHint) -> Catalog:
23
+ raise NotImplementedError
src/catalog/store.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CatalogStore — persists per-user catalogs as Postgres jsonb rows.
2
+
3
+ Storage shape: one row per user in a `catalogs` table with columns
4
+ (user_id PK, data jsonb, schema_version, generated_at, updated_at).
5
+ """
6
+
7
+ from .models import Catalog
8
+
9
+
10
+ class CatalogStore:
11
+ """Read/write catalogs keyed by user_id."""
12
+
13
+ async def get(self, user_id: str) -> Catalog | None:
14
+ raise NotImplementedError
15
+
16
+ async def upsert(self, catalog: Catalog) -> None:
17
+ raise NotImplementedError
18
+
19
+ async def delete(self, user_id: str) -> None:
20
+ raise NotImplementedError
src/catalog/validator.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CatalogValidator — Pydantic + business-rule validation for a catalog.
2
+
3
+ Pydantic handles shape; this layer adds invariants that span fields.
4
+ """
5
+
6
+ from .models import Catalog
7
+
8
+
9
+ class CatalogValidationError(Exception):
10
+ pass
11
+
12
+
13
+ class CatalogValidator:
14
+ """Validates a Catalog beyond Pydantic schema checks.
15
+
16
+ Business rules:
17
+ - All source_ids unique within a user
18
+ - All table_ids unique within a source
19
+ - All column_ids unique within a table
20
+ - foreign_keys (when added) reference existing tables/columns
21
+ """
22
+
23
+ def validate(self, catalog: Catalog) -> None:
24
+ raise NotImplementedError
src/config/prompts/__init__.py ADDED
File without changes
src/config/prompts/catalog_enricher.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Catalog Enricher Prompt
2
+
3
+ Takes raw introspected schema (table names, column names, types, sample values)
4
+ and produces AI-generated descriptions for the source, each table, and each column.
5
+
6
+ One LLM call per source at ingestion time. Used by `src/catalog/enricher.py`.
7
+
8
+ ## System prompt
9
+
10
+ (to be written)
11
+
12
+ ## Output schema
13
+
14
+ Strict JSON matching the catalog Pydantic models in `src/catalog/models.py`.
15
+ Validated by `CatalogValidator` before being persisted.
16
+
17
+ ## Notes
18
+
19
+ - Descriptions should be one-line and factual.
20
+ - Do NOT invent column meanings beyond what the sample values support.
21
+ - For `pii_flag` columns, do NOT include sample values in the description.
src/config/prompts/chatbot_system.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chatbot System Prompt
2
+
3
+ Final answer-formation step. Receives the user question, retrieval context (Cu)
4
+ or query results (from QueryExecutor), and produces the natural-language answer.
5
+
6
+ Used by `src/agents/chatbot.py`. SSE-streamed back to the user.
7
+
8
+ ## System prompt
9
+
10
+ (content to be ported from src/config/agents/system_prompt.md, then rewritten
11
+ to remove references to the old retrieval pipeline and reflect catalog-driven flow)
12
+
13
+ ## Notes
14
+
15
+ - Keep answers grounded in the provided context — no hallucination.
16
+ - For tabular results, format numbers with appropriate units (from `column.unit` when available).
17
+ - Cite sources when applicable (filename + page label).
src/config/prompts/guardrails.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Guardrails
2
+
3
+ Safety / refusal / scope-bounding rules applied to all LLM calls.
4
+
5
+ (content to be ported from src/config/agents/guardrails_prompt.md)
6
+
7
+ ## Scope
8
+
9
+ - Refuse PII extraction requests
10
+ - Refuse questions outside the user's data scope
11
+ - Refuse code-execution / shell-style requests
12
+ - (more to be added)
src/config/prompts/intent_router.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Intent Router Prompt
2
+
3
+ Classifies a user message into:
4
+ - `needs_search`: bool
5
+ - `source_hint`: `chat` | `unstructured` | `structured`
6
+
7
+ Used by `src/agents/intent_router.py`.
8
+
9
+ ## System prompt
10
+
11
+ (to be written)
12
+
13
+ ## Output schema
14
+
15
+ ```json
16
+ {
17
+ "needs_search": true,
18
+ "source_hint": "structured",
19
+ "rewritten_query": "..."
20
+ }
21
+ ```
22
+
23
+ ## Few-shot examples
24
+
25
+ (to be written)
src/config/prompts/query_planner.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Query Planner Prompt
2
+
3
+ Takes a user question + their data catalog (Cs ∪ Ct).
4
+ Produces a JSON IR that describes the query intent.
5
+
6
+ See ARCHITECTURE.md §7 for the IR schema. Used by `src/query/planner/service.py`.
7
+
8
+ ## System prompt
9
+
10
+ (to be written)
11
+
12
+ ## Output schema
13
+
14
+ Strict JSON matching `src/query/ir/models.py:QueryIR`.
15
+ Validated by `IRValidator` against the catalog before reaching the compiler.
16
+
17
+ ## Few-shot examples
18
+
19
+ (to be written — 5–10 examples covering filter + groupby + agg + sort + limit)
20
+
21
+ ## Notes
22
+
23
+ - Reference columns by `column_id`, not `name`.
24
+ - `value_type` must match the column's `data_type`.
25
+ - Only emit operators/aggs from the whitelist (`src/query/ir/operators.py`).
src/models/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API request/response shapes per route family."""
src/models/api/catalog.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Request / response models for catalog-related routes (e.g. /knowledge/rebuild)."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class CatalogRebuildRequest(BaseModel):
7
+ user_id: str
8
+
9
+
10
+ class CatalogRebuildResponse(BaseModel):
11
+ user_id: str
12
+ sources_enriched: int
13
+ tables_enriched: int
14
+ columns_enriched: int
src/models/api/chat.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Request / response models for /api/v1/chat/* routes."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class ChatRequest(BaseModel):
9
+ user_id: str
10
+ room_id: str
11
+ message: str
12
+
13
+
14
+ class ChatStreamEvent(BaseModel):
15
+ """One SSE event. Type values: `sources`, `chunk`, `done`."""
16
+ event: str
17
+ data: dict[str, Any]
src/models/api/document.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Request / response models for /api/v1/documents/* routes."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class DocumentUploadResponse(BaseModel):
7
+ document_id: str
8
+ filename: str
9
+ status: str # uploaded | processing | completed | failed
src/pipeline/document_pipeline.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DocumentPipeline — extract text, chunk, embed, ingest to PGVector.
2
+
3
+ For unstructured sources (PDF / DOCX / TXT). Receives the working
4
+ implementation from the previous pipeline/document_pipeline/document_pipeline.py
5
+ during the cleanup phase.
6
+ """
7
+
8
+
9
+ class DocumentPipeline:
10
+ async def run(self, document_id: str, user_id: str) -> None:
11
+ raise NotImplementedError
src/pipeline/orchestrator.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IngestionOrchestrator — top-level coordinator for ingestion.
2
+
3
+ Routes uploads / DB connections to the right pipeline:
4
+ - unstructured (pdf/docx/txt) → DocumentPipeline
5
+ - schema or tabular → StructuredPipeline (which writes to the catalog)
6
+ """
7
+
8
+
9
+ class IngestionOrchestrator:
10
+ async def ingest(self, source_ref: str, source_type: str, user_id: str) -> None:
11
+ raise NotImplementedError
src/pipeline/structured_pipeline.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """StructuredPipeline — runs catalog enrichment for DB / tabular sources.
2
+
3
+ Steps:
4
+ 1. introspect (catalog/introspect/database.py or catalog/introspect/tabular.py)
5
+ 2. enrich (catalog/enricher.py — 1 LLM call per source)
6
+ 3. validate (catalog/validator.py)
7
+ 4. write (catalog/store.py)
8
+ """
9
+
10
+
11
+ class StructuredPipeline:
12
+ async def run(self, source_ref: str, source_type: str, user_id: str) -> None:
13
+ raise NotImplementedError
src/pipeline/triggers.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pipeline trigger entry points called from API routes / event handlers.
2
+
3
+ These thin functions are what the FastAPI routes invoke; they delegate
4
+ to IngestionOrchestrator.
5
+ """
6
+
7
+
8
+ async def on_document_uploaded(document_id: str, user_id: str) -> None:
9
+ raise NotImplementedError
10
+
11
+
12
+ async def on_db_registered(database_client_id: str, user_id: str) -> None:
13
+ raise NotImplementedError
14
+
15
+
16
+ async def on_tabular_uploaded(document_id: str, user_id: str) -> None:
17
+ raise NotImplementedError
18
+
19
+
20
+ async def on_catalog_rebuild_requested(user_id: str) -> None:
21
+ raise NotImplementedError
src/query/compiler/base.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BaseCompiler — contract for IR → executable shape (SQL string or pandas chain)."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from ..ir.models import QueryIR
6
+
7
+
8
+ class BaseCompiler(ABC):
9
+ """Subclasses: SqlCompiler, PandasCompiler."""
10
+
11
+ @abstractmethod
12
+ def compile(self, ir: QueryIR) -> object:
13
+ ...
src/query/compiler/pandas.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PandasCompiler — IR → callable that runs against a DataFrame.
2
+
3
+ For tabular sources. The callable encapsulates the chain of operations
4
+ (filter → groupby → agg → sort → limit) so the executor can apply them
5
+ to a DataFrame loaded eagerly or via predicate pushdown / polars lazy scan.
6
+ """
7
+
8
+ from typing import Callable
9
+
10
+ from ...catalog.models import Catalog
11
+ from ..ir.models import QueryIR
12
+ from .base import BaseCompiler
13
+
14
+
15
+ class PandasCompiler(BaseCompiler):
16
+ """Deterministic IR → pandas/polars op chain. No LLM."""
17
+
18
+ def __init__(self, catalog: Catalog) -> None:
19
+ self._catalog = catalog
20
+
21
+ def compile(self, ir: QueryIR) -> Callable:
22
+ raise NotImplementedError
src/query/compiler/sql.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SqlCompiler — IR → (SQL string, parameters list).
2
+
3
+ Identifiers (table, column names) come from the catalog (trusted).
4
+ Values come from IR.filters and are ALWAYS parameterized — never inlined.
5
+ Output is validated by sqlglot before reaching the executor.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from ...catalog.models import Catalog
11
+ from ..ir.models import QueryIR
12
+ from .base import BaseCompiler
13
+
14
+
15
+ @dataclass
16
+ class CompiledSql:
17
+ sql: str
18
+ params: list[object]
19
+
20
+
21
+ class SqlCompiler(BaseCompiler):
22
+ """Deterministic IR → SQL. No LLM."""
23
+
24
+ def __init__(self, catalog: Catalog) -> None:
25
+ self._catalog = catalog
26
+
27
+ def compile(self, ir: QueryIR) -> CompiledSql:
28
+ raise NotImplementedError
src/query/executor/base.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BaseExecutor + QueryResult — uniform return shape across DB and tabular paths."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from ..ir.models import QueryIR
8
+
9
+
10
+ @dataclass
11
+ class QueryResult:
12
+ source_id: str
13
+ backend: str # "sql" | "tabular"
14
+ rows: list[dict[str, Any]] = field(default_factory=list)
15
+ row_count: int = 0
16
+ truncated: bool = False
17
+ elapsed_ms: int = 0
18
+ error: str | None = None
19
+
20
+
21
+ class BaseExecutor(ABC):
22
+ """Subclasses: DbExecutor, TabularExecutor."""
23
+
24
+ @abstractmethod
25
+ async def run(self, ir: QueryIR) -> QueryResult:
26
+ ...
src/query/executor/db.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DbExecutor — runs compiled SQL on a user's external DB.
2
+
3
+ Pipeline:
4
+ IR → SqlCompiler → SQL string + params
5
+
6
+ sqlglot validation (SELECT-only, whitelist tables/columns, LIMIT enforced)
7
+
8
+ asyncpg / pymysql in read-only transaction with timeout (30s)
9
+
10
+ QueryResult
11
+ """
12
+
13
+ from ...catalog.models import Catalog
14
+ from ..compiler.sql import SqlCompiler
15
+ from ..ir.models import QueryIR
16
+ from .base import BaseExecutor, QueryResult
17
+
18
+
19
+ class DbExecutor(BaseExecutor):
20
+ def __init__(self, catalog: Catalog) -> None:
21
+ self._catalog = catalog
22
+ self._compiler = SqlCompiler(catalog)
23
+
24
+ async def run(self, ir: QueryIR) -> QueryResult:
25
+ raise NotImplementedError
src/query/executor/dispatcher.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Picks DB vs Tabular executor based on the source_type of the IR's source.
2
+
3
+ This is the only place in the structured query path where the schema/tabular
4
+ distinction matters. Every step before this is source-type-agnostic.
5
+ """
6
+
7
+ from ...catalog.models import Catalog
8
+ from ..ir.models import QueryIR
9
+ from .base import BaseExecutor
10
+
11
+
12
+ class ExecutorDispatcher:
13
+ def __init__(self, catalog: Catalog) -> None:
14
+ self._catalog = catalog
15
+
16
+ def pick(self, ir: QueryIR) -> BaseExecutor:
17
+ raise NotImplementedError
src/query/executor/tabular.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TabularExecutor — runs compiled pandas/polars chain on a Parquet file.
2
+
3
+ Picks engine by file size:
4
+ ≤ 100 MB → eager pandas
5
+ 100 MB-1 GB → pyarrow with predicate pushdown
6
+ > 1 GB → polars lazy scan
7
+
8
+ Initial scope ships eager pandas only; the others are added when a real
9
+ file is too big.
10
+ """
11
+
12
+ from ...catalog.models import Catalog
13
+ from ..compiler.pandas import PandasCompiler
14
+ from ..ir.models import QueryIR
15
+ from .base import BaseExecutor, QueryResult
16
+
17
+
18
+ class TabularExecutor(BaseExecutor):
19
+ def __init__(self, catalog: Catalog) -> None:
20
+ self._catalog = catalog
21
+ self._compiler = PandasCompiler(catalog)
22
+
23
+ async def run(self, ir: QueryIR) -> QueryResult:
24
+ raise NotImplementedError
src/query/ir/models.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """JSON IR (intermediate representation) Pydantic models.
2
+
3
+ See ARCHITECTURE.md §7 for the schema.
4
+
5
+ Initial scope: single-table; filter, group_by, agg, order_by, limit.
6
+ Joins, having, offset, boolean tree filters are deferred to later versions.
7
+ """
8
+
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ FilterOp = Literal[
15
+ "=", "!=", "<", "<=", ">", ">=",
16
+ "in", "not_in", "is_null", "is_not_null",
17
+ "like", "between",
18
+ ]
19
+ AggFn = Literal["count", "count_distinct", "sum", "avg", "min", "max"]
20
+ ValueType = Literal["int", "decimal", "string", "datetime", "date", "bool"]
21
+ SortDir = Literal["asc", "desc"]
22
+
23
+
24
+ class ColumnSelect(BaseModel):
25
+ kind: Literal["column"] = "column"
26
+ column_id: str
27
+ alias: str | None = None
28
+
29
+
30
+ class AggSelect(BaseModel):
31
+ kind: Literal["agg"] = "agg"
32
+ fn: AggFn
33
+ column_id: str | None = None
34
+ alias: str | None = None
35
+
36
+
37
+ SelectItem = ColumnSelect | AggSelect
38
+
39
+
40
+ class FilterClause(BaseModel):
41
+ column_id: str
42
+ op: FilterOp
43
+ value: Any
44
+ value_type: ValueType
45
+
46
+
47
+ class OrderByClause(BaseModel):
48
+ column_id: str
49
+ dir: SortDir = "asc"
50
+
51
+
52
+ class QueryIR(BaseModel):
53
+ ir_version: str = "1.0"
54
+ source_id: str
55
+ table_id: str
56
+ select: list[SelectItem]
57
+ filters: list[FilterClause] = Field(default_factory=list)
58
+ group_by: list[str] = Field(default_factory=list)
59
+ order_by: list[OrderByClause] = Field(default_factory=list)
60
+ limit: int | None = None
src/query/ir/operators.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Whitelisted operators + aggregation functions for IR validation."""
2
+
3
+ ALLOWED_FILTER_OPS = frozenset({
4
+ "=", "!=", "<", "<=", ">", ">=",
5
+ "in", "not_in", "is_null", "is_not_null",
6
+ "like", "between",
7
+ })
8
+
9
+ ALLOWED_AGG_FNS = frozenset({
10
+ "count", "count_distinct", "sum", "avg", "min", "max",
11
+ })
12
+
13
+ LIMIT_HARD_CAP = 10_000
14
+
15
+ # Type compatibility: which value_types are valid for each column data_type.
16
+ # To be filled with the explicit matrix when validator.py is implemented.
17
+ TYPE_COMPATIBILITY: dict[str, frozenset[str]] = {}
src/query/ir/validator.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IRValidator — checks a QueryIR against a user's catalog.
2
+
3
+ See ARCHITECTURE.md §7 for the validation rules.
4
+ On failure, the planner is re-prompted with the error context (max 3 retries).
5
+ """
6
+
7
+ from ...catalog.models import Catalog
8
+ from .models import QueryIR
9
+
10
+
11
+ class IRValidationError(Exception):
12
+ pass
13
+
14
+
15
+ class IRValidator:
16
+ """Reject IRs that reference unknown sources/tables/columns or use disallowed ops.
17
+
18
+ Rules:
19
+ - source_id exists in catalog for this user
20
+ - table_id belongs to that source
21
+ - every column_id exists in that table
22
+ - every agg.fn and filter.op is whitelisted (see operators.py)
23
+ - value_type consistent with column.data_type
24
+ - limit positive int, ≤ hard cap
25
+ """
26
+
27
+ def validate(self, ir: QueryIR, catalog: Catalog) -> None:
28
+ raise NotImplementedError
src/query/planner/prompt.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Builds the planner LLM prompt from question + catalog.
2
+
3
+ Renders the catalog into a compact textual form that fits the LLM context
4
+ window. For users with ≤50 tables the full catalog goes in verbatim.
5
+ """
6
+
7
+ from ...catalog.models import Catalog
8
+
9
+
10
+ def build_planner_prompt(question: str, catalog: Catalog) -> str:
11
+ """Return the full prompt string to feed the planner LLM."""
12
+ raise NotImplementedError
src/query/planner/service.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """QueryPlannerService — single LLM call: question + catalog → JSON IR.
2
+
3
+ Prompt: src/config/prompts/query_planner.md
4
+ Output: a QueryIR ready for the IRValidator.
5
+ """
6
+
7
+ from ...catalog.models import Catalog
8
+ from ..ir.models import QueryIR
9
+
10
+
11
+ class QueryPlannerService:
12
+ """Wraps the LLM call with structured-output parsing into QueryIR."""
13
+
14
+ async def plan(self, question: str, catalog: Catalog) -> QueryIR:
15
+ raise NotImplementedError
src/query/service.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """QueryService — orchestrates plan → validate → compile → execute.
2
+
3
+ Top-level entry point for catalog-driven structured queries. Wired into
4
+ the chat endpoint when source_hint == "structured".
5
+ """
6
+
7
+ from ..catalog.models import Catalog
8
+ from .executor.base import QueryResult
9
+
10
+
11
+ class QueryService:
12
+ """End-to-end runner for a user question against a catalog."""
13
+
14
+ async def run(self, user_id: str, question: str, catalog: Catalog) -> QueryResult:
15
+ raise NotImplementedError
src/retrieval/document.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DocumentRetriever — dense similarity over prose chunks (Cu).
2
+
3
+ For unstructured sources only (PDF / DOCX / TXT). Backed by PGVector with
4
+ collection `document_embeddings`. Methods: MMR, cosine, euclidean, etc.
5
+
6
+ Receives the working implementation from the previous src/rag/retrievers/document.py
7
+ during the cleanup phase; for now this is a placeholder.
8
+ """
9
+
10
+
11
+ class DocumentRetriever:
12
+ """Dense retrieval over PGVector chunks for unstructured sources."""
13
+
14
+ async def retrieve(self, query: str, user_id: str, k: int = 5) -> list:
15
+ raise NotImplementedError
src/retrieval/router.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Retrieval-side router.
2
+
3
+ Currently dispatches only the `unstructured` route to DocumentRetriever.
4
+ The `structured` route is owned by query/service.py — not by retrieval.
5
+ The `chat` route bypasses retrieval entirely.
6
+ """
7
+
8
+
9
+ class RetrievalRouter:
10
+ async def dispatch(self, query: str, user_id: str, source_hint: str) -> list:
11
+ raise NotImplementedError
src/security/auth.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Authentication helpers: password hashing, JWT encode/decode, get_user.
2
+
3
+ Receives the working implementation from the previous src/users/users.py
4
+ during the cleanup phase.
5
+ """
6
+
7
+
8
+ def hash_password(plaintext: str) -> str:
9
+ raise NotImplementedError
10
+
11
+
12
+ def verify_password(plaintext: str, hashed: str) -> bool:
13
+ raise NotImplementedError
14
+
15
+
16
+ def encode_jwt(payload: dict) -> str:
17
+ raise NotImplementedError
18
+
19
+
20
+ def decode_jwt(token: str) -> dict:
21
+ raise NotImplementedError
src/security/credentials.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fernet-encrypted credential storage for user-registered DB connections.
2
+
3
+ Receives the working implementation from the previous
4
+ src/utils/db_credential_encryption.py during the cleanup phase.
5
+
6
+ Key: settings.dataeyond__db__credential__key (Fernet, kept out of source).
7
+ """
8
+
9
+
10
+ def encrypt_credential(plaintext: str) -> str:
11
+ raise NotImplementedError
12
+
13
+
14
+ def decrypt_credential(ciphertext: str) -> str:
15
+ raise NotImplementedError
src/security/pii_patterns.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Regex patterns and column-name heuristics for PII detection.
2
+
3
+ Used by catalog/pii_detector.py at ingestion time. Default policy:
4
+ when in doubt, set pii_flag=True. False positives cost nothing; false
5
+ negatives leak data.
6
+ """
7
+
8
+ import re
9
+
10
+ PII_NAME_PATTERNS = frozenset({
11
+ "email",
12
+ "phone", "mobile", "telp", "telephone",
13
+ "ssn", "tin", "passport", "ktp", "nik",
14
+ "name", "fullname", "first_name", "last_name", "surname",
15
+ "address", "street", "zipcode", "postal",
16
+ "birthdate", "dob", "birthday",
17
+ })
18
+
19
+ EMAIL_REGEX = re.compile(r"^[\w.+-]+@[\w-]+\.[\w.-]+$")
20
+ PHONE_REGEX = re.compile(r"^\+?[\d\s\-()]{7,}$")