mcp-agentic-pipelines 1.0.1
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.
- package/.env.example +93 -0
- package/README.md +258 -0
- package/package.json +70 -0
- package/packages/clinical/package.json +22 -0
- package/packages/clinical/src/index.ts +262 -0
- package/packages/clinical/tsconfig.json +13 -0
- package/packages/core/package.json +21 -0
- package/packages/core/src/config.ts +138 -0
- package/packages/core/src/errors.ts +100 -0
- package/packages/core/src/index.ts +104 -0
- package/packages/core/src/llm-config.ts +213 -0
- package/packages/core/src/logging.ts +66 -0
- package/packages/core/src/python-bridge.ts +384 -0
- package/packages/core/src/rate-limiter.ts +136 -0
- package/packages/core/src/types.ts +203 -0
- package/packages/core/src/validation.ts +101 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/deeppipe/package.json +21 -0
- package/packages/deeppipe/src/index.ts +424 -0
- package/packages/deeppipe/tsconfig.json +13 -0
- package/packages/piste/package.json +20 -0
- package/packages/piste/src/index.ts +48 -0
- package/packages/piste/tsconfig.json +13 -0
- package/packages/precis/package.json +20 -0
- package/packages/precis/src/index.ts +67 -0
- package/packages/precis/tsconfig.json +13 -0
- package/packages/server/package.json +31 -0
- package/packages/server/src/index.ts +427 -0
- package/packages/server/tsconfig.json +17 -0
- package/setup.mjs +141 -0
- package/test.mjs +337 -0
- package/vendors/clinical-intake/pipeline.mjs +349 -0
- package/vendors/clinical-intake/questions/en.txt +9 -0
- package/vendors/clinical-intake/questions/fr.txt +9 -0
- package/vendors/piste/.env.example +73 -0
- package/vendors/piste/app/core/__init__.py +4 -0
- package/vendors/piste/app/core/config.py +83 -0
- package/vendors/piste/app/core/debuglog.py +16 -0
- package/vendors/piste/app/core/middleware.py +40 -0
- package/vendors/piste/bridge_piste.py +301 -0
- package/vendors/piste/pipeline/__init__.py +4 -0
- package/vendors/piste/pipeline/compiler.py +68 -0
- package/vendors/piste/pipeline/offline/__init__.py +28 -0
- package/vendors/piste/pipeline/offline/verifaid_pipeline.py +247 -0
- package/vendors/piste/pipeline/replay.py +15 -0
- package/vendors/piste/pipeline/replay_engine.py +249 -0
- package/vendors/piste/pipeline/signatures/__init__.py +4 -0
- package/vendors/piste/pipeline/signatures/signatures.py +136 -0
- package/vendors/piste/pipeline/stage1/__init__.py +21 -0
- package/vendors/piste/pipeline/stage1/atomic_decomposer.py +61 -0
- package/vendors/piste/pipeline/stage1/check_worthiness.py +100 -0
- package/vendors/piste/pipeline/stage1/orchestrator.py +175 -0
- package/vendors/piste/pipeline/stage1/test_stage1.py +162 -0
- package/vendors/piste/pipeline/stage2/__init__.py +34 -0
- package/vendors/piste/pipeline/stage2/blind_retriever.py +303 -0
- package/vendors/piste/pipeline/stage2/canonical_mapper.py +124 -0
- package/vendors/piste/pipeline/stage2/credibility_scorer.py +85 -0
- package/vendors/piste/pipeline/stage2/orchestrator.py +311 -0
- package/vendors/piste/pipeline/stage2/query_refiner.py +88 -0
- package/vendors/piste/pipeline/stage2/search_decision.py +69 -0
- package/vendors/piste/pipeline/stage2/test_stage2.py +265 -0
- package/vendors/piste/pipeline/stage3/__init__.py +20 -0
- package/vendors/piste/pipeline/stage3/classifier.py +79 -0
- package/vendors/piste/pipeline/stage3/orchestrator.py +225 -0
- package/vendors/piste/pipeline/stage3/test_stage3.py +101 -0
- package/vendors/piste/pipeline/stage4/__init__.py +33 -0
- package/vendors/piste/pipeline/stage4/criticality_gate.py +177 -0
- package/vendors/piste/pipeline/stage4/orchestrator.py +269 -0
- package/vendors/piste/pipeline/stage4/test_stage4.py +192 -0
- package/vendors/piste/pipeline/stage4/verdict_aggregator.py +157 -0
- package/vendors/piste/requirements.txt +53 -0
- package/vendors/precis/backend/__init__.py +6 -0
- package/vendors/precis/backend/agents/__init__.py +3 -0
- package/vendors/precis/backend/agents/data_synthesis.py +105 -0
- package/vendors/precis/backend/agents/dist_free_synth.py +97 -0
- package/vendors/precis/backend/agents/exact_hash_retriever.py +327 -0
- package/vendors/precis/backend/agents/fusion_ranker.py +64 -0
- package/vendors/precis/backend/agents/guardrail.py +175 -0
- package/vendors/precis/backend/agents/query_expander.py +89 -0
- package/vendors/precis/backend/agents/radial_interpol.py +99 -0
- package/vendors/precis/backend/agents/report_generator.py +92 -0
- package/vendors/precis/backend/agents/semantic_reranker.py +135 -0
- package/vendors/precis/backend/agents/stat_anomaly.py +93 -0
- package/vendors/precis/backend/agents/vector_index.py +123 -0
- package/vendors/precis/backend/agents/veri_score.py +341 -0
- package/vendors/precis/backend/agents/work_order_extractor.py +205 -0
- package/vendors/precis/backend/api/__init__.py +3 -0
- package/vendors/precis/backend/api/routes/__init__.py +3 -0
- package/vendors/precis/backend/config.py +88 -0
- package/vendors/precis/backend/core/__init__.py +13 -0
- package/vendors/precis/backend/core/hashing.py +22 -0
- package/vendors/precis/backend/core/metrics.py +77 -0
- package/vendors/precis/backend/core/multitoken.py +166 -0
- package/vendors/precis/backend/core/pmi.py +54 -0
- package/vendors/precis/backend/core/stemming.py +74 -0
- package/vendors/precis/backend/core/tracing.py +150 -0
- package/vendors/precis/backend/data/__init__.py +3 -0
- package/vendors/precis/backend/data/chunker.py +57 -0
- package/vendors/precis/backend/data/pdf_parser.py +42 -0
- package/vendors/precis/backend/db/__init__.py +3 -0
- package/vendors/precis/backend/db/models.py +173 -0
- package/vendors/precis/backend/db/repository.py +269 -0
- package/vendors/precis/backend/llm/__init__.py +3 -0
- package/vendors/precis/backend/llm/anthropic_provider.py +39 -0
- package/vendors/precis/backend/llm/base.py +147 -0
- package/vendors/precis/backend/llm/deepseek_provider.py +43 -0
- package/vendors/precis/backend/llm/factory.py +60 -0
- package/vendors/precis/backend/llm/google_provider.py +39 -0
- package/vendors/precis/backend/llm/ollama_provider.py +54 -0
- package/vendors/precis/backend/llm/openai_provider.py +50 -0
- package/vendors/precis/backend/main.py +677 -0
- package/vendors/precis/backend/orchestrator/__init__.py +3 -0
- package/vendors/precis/backend/orchestrator/planner.py +81 -0
- package/vendors/precis/backend/orchestrator/router.py +319 -0
- package/vendors/precis/backend/orchestrator/types.py +58 -0
- package/vendors/precis/bridge_precis.py +185 -0
- package/vendors/precis/data/sample_reports/README.md +8 -0
- package/vendors/precis/data/seed_data.py +115 -0
- package/vendors/precis/requirements.txt +19 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# © JINAN KORDAB — 2026 AI HYBRID AGENTIC RETRIEVAL-AUGMENTED GENERATION RAG PIPELINE - PERSONAL PROJECT
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# All persistent entities in the Precis system.
|
|
5
|
+
# Single SQLite database file: data/app.db
|
|
6
|
+
#
|
|
7
|
+
# Tables:
|
|
8
|
+
# execution_traces — Complete audit trail per query (JSON events)
|
|
9
|
+
# conversation_turns— Conversation history per session
|
|
10
|
+
# document_index — Metadata about indexed documents
|
|
11
|
+
# agent_metrics — Aggregated per-agent performance over time
|
|
12
|
+
#
|
|
13
|
+
# Related:
|
|
14
|
+
# backend/db/repository.py — Data access layer
|
|
15
|
+
# backend/core/tracing.py — TraceCollector (produces trace data)
|
|
16
|
+
# backend/orchestrator/memory.py — Conversation turn storage
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from typing import Optional
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
from sqlalchemy import Column, String, Integer, Float, Text, DateTime, ForeignKey, Index
|
|
24
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Base(DeclarativeBase):
|
|
28
|
+
"""Base class for all SQLAlchemy ORM models."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# ExecutionTrace — The User-Facing Audit Trail
|
|
34
|
+
# =============================================================================
|
|
35
|
+
|
|
36
|
+
class ExecutionTrace(Base):
|
|
37
|
+
"""
|
|
38
|
+
Stores the complete audit trail for a single query execution.
|
|
39
|
+
|
|
40
|
+
Each row = one user query, with ALL agent decision events stored
|
|
41
|
+
as a JSON array in the events_json column.
|
|
42
|
+
|
|
43
|
+
Why one JSON column instead of normalized event rows?
|
|
44
|
+
- Write pattern: All events for a query are generated together.
|
|
45
|
+
One INSERT is faster and simpler than N INSERTs.
|
|
46
|
+
- Read pattern: User always requests the FULL trace for a query.
|
|
47
|
+
No need to join — just SELECT the JSON column.
|
|
48
|
+
- Query pattern: When you DO need to analyze across traces,
|
|
49
|
+
SQLite's json_extract() function can query into the JSON.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
__tablename__ = "execution_traces"
|
|
53
|
+
|
|
54
|
+
# Primary key
|
|
55
|
+
trace_id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
56
|
+
|
|
57
|
+
# Links
|
|
58
|
+
query_id = Column(String(36), nullable=False, index=True)
|
|
59
|
+
session_id = Column(String(36), nullable=True, index=True)
|
|
60
|
+
|
|
61
|
+
# Content
|
|
62
|
+
query_text = Column(Text, nullable=False) # The original user query
|
|
63
|
+
events_json = Column(Text, nullable=False) # Full trace as JSON string
|
|
64
|
+
status = Column(String(20), nullable=False, default="success") # success | error
|
|
65
|
+
|
|
66
|
+
# Metrics
|
|
67
|
+
duration_ms = Column(Integer, nullable=False, default=0)
|
|
68
|
+
agent_count = Column(Integer, nullable=False, default=0) # How many agents executed
|
|
69
|
+
event_count = Column(Integer, nullable=False, default=0) # How many trace events
|
|
70
|
+
|
|
71
|
+
# Timestamps
|
|
72
|
+
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
73
|
+
|
|
74
|
+
# Index for fast lookup: find all traces for a session
|
|
75
|
+
__table_args__ = (
|
|
76
|
+
Index("idx_traces_session", "session_id", "created_at"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# =============================================================================
|
|
81
|
+
# ConversationTurn — Persistent Conversation History
|
|
82
|
+
# =============================================================================
|
|
83
|
+
|
|
84
|
+
class ConversationTurn(Base):
|
|
85
|
+
"""
|
|
86
|
+
Stores a single turn (query + response pair) in a conversation.
|
|
87
|
+
|
|
88
|
+
Used by MemoryAgent (backend/orchestrator/memory.py) for Tier 2 persistence.
|
|
89
|
+
Tier 1 is in-memory OrderedDict. Tier 2 is this SQLite table.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
__tablename__ = "conversation_turns"
|
|
93
|
+
|
|
94
|
+
turn_id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
95
|
+
session_id = Column(String(36), nullable=False, index=True)
|
|
96
|
+
query_text = Column(Text, nullable=False)
|
|
97
|
+
response_summary = Column(Text, nullable=True) # LLM-generated compact summary
|
|
98
|
+
trace_id = Column(String(36), nullable=True) # Link to full execution trace
|
|
99
|
+
plan_used = Column(Text, nullable=True) # JSON of the ExecutionPlan
|
|
100
|
+
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# DocumentIndex — Metadata About Indexed Documents
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
class DocumentIndex(Base):
|
|
108
|
+
"""
|
|
109
|
+
Tracks documents that have been ingested into the ExactHash index.
|
|
110
|
+
|
|
111
|
+
Used to avoid re-indexing documents and to show the user what's available.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
__tablename__ = "document_index"
|
|
115
|
+
|
|
116
|
+
doc_id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
117
|
+
filename = Column(String(500), nullable=False, unique=True)
|
|
118
|
+
file_hash = Column(String(64), nullable=False) # SHA-256 of file contents
|
|
119
|
+
page_count = Column(Integer, nullable=False, default=0)
|
|
120
|
+
multitoken_count = Column(Integer, nullable=False, default=0)
|
|
121
|
+
document_text = Column(Text, nullable=True) # Original text for re-indexing on restart
|
|
122
|
+
corpus_name = Column(String(100), nullable=False, default="default")
|
|
123
|
+
indexed_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# =============================================================================
|
|
127
|
+
# AgentMetrics — Aggregated Performance Over Time
|
|
128
|
+
# =============================================================================
|
|
129
|
+
|
|
130
|
+
class AgentMetrics(Base):
|
|
131
|
+
"""
|
|
132
|
+
Aggregated performance metrics per agent, updated periodically.
|
|
133
|
+
|
|
134
|
+
Feeds the Evaluation Dashboard (frontend/src/app/evaluate/page.tsx)
|
|
135
|
+
and AgentStatusCard components.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
__tablename__ = "agent_metrics"
|
|
139
|
+
|
|
140
|
+
metric_id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
141
|
+
agent_name = Column(String(50), nullable=False, index=True)
|
|
142
|
+
metric_name = Column(String(50), nullable=False) # "avg_latency_ms", "accuracy", etc.
|
|
143
|
+
metric_value = Column(Float, nullable=False)
|
|
144
|
+
sample_count = Column(Integer, nullable=False, default=0)
|
|
145
|
+
computed_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# =============================================================================
|
|
149
|
+
# WorkOrder — Extracted Aviation MRO Work Orders
|
|
150
|
+
# =============================================================================
|
|
151
|
+
|
|
152
|
+
class WorkOrderRecord(Base):
|
|
153
|
+
"""Structured work order extracted from aviation maintenance documents."""
|
|
154
|
+
|
|
155
|
+
__tablename__ = "work_orders"
|
|
156
|
+
|
|
157
|
+
wo_id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
158
|
+
source_file = Column(String(500), nullable=False)
|
|
159
|
+
tail_number = Column(String(20), nullable=True, index=True)
|
|
160
|
+
work_order_number = Column(String(50), nullable=True, index=True)
|
|
161
|
+
date = Column(String(30), nullable=True)
|
|
162
|
+
aircraft_model = Column(String(30), nullable=True)
|
|
163
|
+
part_numbers = Column(Text, nullable=True) # JSON array
|
|
164
|
+
part_descriptions = Column(Text, nullable=True) # JSON array
|
|
165
|
+
serial_numbers = Column(Text, nullable=True) # JSON array
|
|
166
|
+
mechanic_id = Column(String(30), nullable=True, index=True)
|
|
167
|
+
station = Column(String(10), nullable=True, index=True)
|
|
168
|
+
work_performed = Column(Text, nullable=True)
|
|
169
|
+
hours_worked = Column(String(20), nullable=True)
|
|
170
|
+
ad_sb_references = Column(Text, nullable=True) # JSON array
|
|
171
|
+
inspector_stamp = Column(String(50), nullable=True)
|
|
172
|
+
extracted_fields_json = Column(Text, nullable=True) # Full extraction trace
|
|
173
|
+
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""© JINAN KORDAB — 2026 AI HYBRID AGENTIC RETRIEVAL-AUGMENTED GENERATION RAG PIPELINE - PERSONAL PROJECT"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import create_engine, desc
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from backend.db.models import Base, ExecutionTrace, ConversationTurn, DocumentIndex, AgentMetrics
|
|
12
|
+
from backend.config import settings
|
|
13
|
+
|
|
14
|
+
_engine = create_engine(
|
|
15
|
+
settings.DATABASE_URL,
|
|
16
|
+
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {},
|
|
17
|
+
echo=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def init_db() -> None:
|
|
22
|
+
"""Create all tables. Safe to call multiple times."""
|
|
23
|
+
Base.metadata.create_all(bind=_engine)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_session() -> Session:
|
|
27
|
+
return Session(_engine)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── ExecutionTrace ─────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
def save_trace(query_id: str, query_text: str, events_json: str,
|
|
33
|
+
session_id: Optional[str] = None, status: str = "success",
|
|
34
|
+
duration_ms: int = 0, agent_count: int = 0, event_count: int = 0) -> str:
|
|
35
|
+
"""Persist a completed execution trace. Returns trace_id."""
|
|
36
|
+
trace_id = str(uuid.uuid4())
|
|
37
|
+
with get_session() as session:
|
|
38
|
+
trace = ExecutionTrace(
|
|
39
|
+
trace_id=trace_id, query_id=query_id, session_id=session_id,
|
|
40
|
+
query_text=query_text, events_json=events_json, status=status,
|
|
41
|
+
duration_ms=duration_ms, agent_count=agent_count, event_count=event_count,
|
|
42
|
+
)
|
|
43
|
+
session.add(trace)
|
|
44
|
+
session.commit()
|
|
45
|
+
return trace_id
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_trace(trace_id: str) -> Optional[Dict]:
|
|
49
|
+
"""Retrieve a trace by ID. Returns dict or None."""
|
|
50
|
+
with get_session() as session:
|
|
51
|
+
row = session.get(ExecutionTrace, trace_id)
|
|
52
|
+
if row is None:
|
|
53
|
+
return None
|
|
54
|
+
return {
|
|
55
|
+
"trace_id": row.trace_id, "query_id": row.query_id,
|
|
56
|
+
"query_text": row.query_text, "status": row.status,
|
|
57
|
+
"duration_ms": row.duration_ms, "agent_count": row.agent_count,
|
|
58
|
+
"event_count": row.event_count,
|
|
59
|
+
"events_json": json.loads(row.events_json) if row.events_json else {},
|
|
60
|
+
"created_at": row.created_at.isoformat() if row.created_at else "",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_traces_for_session(session_id: str, limit: int = 50) -> List[Dict]:
|
|
65
|
+
"""Get all traces for a session, most recent first."""
|
|
66
|
+
with get_session() as session:
|
|
67
|
+
rows = (session.query(ExecutionTrace)
|
|
68
|
+
.filter(ExecutionTrace.session_id == session_id)
|
|
69
|
+
.order_by(desc(ExecutionTrace.created_at))
|
|
70
|
+
.limit(limit).all())
|
|
71
|
+
return [_trace_summary(r) for r in rows]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_recent_traces(limit: int = 20) -> List[Dict]:
|
|
75
|
+
with get_session() as session:
|
|
76
|
+
rows = (session.query(ExecutionTrace)
|
|
77
|
+
.order_by(desc(ExecutionTrace.created_at)).limit(limit).all())
|
|
78
|
+
return [_trace_summary(r) for r in rows]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _trace_summary(row: ExecutionTrace) -> Dict:
|
|
82
|
+
return {"trace_id": row.trace_id, "query_id": row.query_id,
|
|
83
|
+
"query_text": row.query_text[:200], "status": row.status,
|
|
84
|
+
"duration_ms": row.duration_ms, "agent_count": row.agent_count,
|
|
85
|
+
"created_at": row.created_at.isoformat() if row.created_at else ""}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── Conversation ───────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
def save_conversation_turn(session_id: str, query_text: str, response_summary: str,
|
|
91
|
+
trace_id: Optional[str] = None, plan_used: Optional[str] = None) -> str:
|
|
92
|
+
turn_id = str(uuid.uuid4())
|
|
93
|
+
with get_session() as session:
|
|
94
|
+
turn = ConversationTurn(turn_id=turn_id, session_id=session_id,
|
|
95
|
+
query_text=query_text, response_summary=response_summary,
|
|
96
|
+
trace_id=trace_id, plan_used=plan_used)
|
|
97
|
+
session.add(turn)
|
|
98
|
+
session.commit()
|
|
99
|
+
return turn_id
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_conversation_history(session_id: str, limit: int = 50) -> List[Dict]:
|
|
103
|
+
with get_session() as session:
|
|
104
|
+
rows = (session.query(ConversationTurn)
|
|
105
|
+
.filter(ConversationTurn.session_id == session_id)
|
|
106
|
+
.order_by(desc(ConversationTurn.created_at)).limit(limit).all())
|
|
107
|
+
return [{"turn_id": r.turn_id, "query_text": r.query_text,
|
|
108
|
+
"response_summary": r.response_summary,
|
|
109
|
+
"trace_id": r.trace_id, "created_at": r.created_at.isoformat() if r.created_at else ""}
|
|
110
|
+
for r in rows]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── Document Index ─────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def register_document(filename: str, file_hash: str, page_count: int,
|
|
116
|
+
multitoken_count: int, corpus_name: str = "default") -> str:
|
|
117
|
+
with get_session() as session:
|
|
118
|
+
existing = session.query(DocumentIndex).filter(DocumentIndex.file_hash == file_hash).first()
|
|
119
|
+
if existing:
|
|
120
|
+
return existing.doc_id
|
|
121
|
+
doc_id = str(uuid.uuid4())
|
|
122
|
+
doc = DocumentIndex(doc_id=doc_id, filename=filename, file_hash=file_hash,
|
|
123
|
+
page_count=page_count, multitoken_count=multitoken_count,
|
|
124
|
+
corpus_name=corpus_name)
|
|
125
|
+
session.add(doc)
|
|
126
|
+
session.commit()
|
|
127
|
+
return doc_id
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def register_document_simple(filename: str, content_hash: str = "", multi_token_count: int = 0,
|
|
131
|
+
corpus_name: str = "default", document_text: str = "") -> str:
|
|
132
|
+
"""Lightweight registration for uploaded docs (no page count needed)."""
|
|
133
|
+
with get_session() as session:
|
|
134
|
+
existing = session.query(DocumentIndex).filter(
|
|
135
|
+
DocumentIndex.filename == filename,
|
|
136
|
+
DocumentIndex.corpus_name == corpus_name
|
|
137
|
+
).first()
|
|
138
|
+
if existing:
|
|
139
|
+
existing.multitoken_count = multi_token_count
|
|
140
|
+
if document_text:
|
|
141
|
+
existing.document_text = document_text
|
|
142
|
+
session.commit()
|
|
143
|
+
return existing.doc_id
|
|
144
|
+
doc_id = str(uuid.uuid4())
|
|
145
|
+
doc = DocumentIndex(doc_id=doc_id, filename=filename,
|
|
146
|
+
file_hash=content_hash or filename,
|
|
147
|
+
page_count=1, multitoken_count=multi_token_count,
|
|
148
|
+
corpus_name=corpus_name, document_text=document_text)
|
|
149
|
+
session.add(doc)
|
|
150
|
+
session.commit()
|
|
151
|
+
return doc_id
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_all_documents(corpus_name: str = "default") -> list:
|
|
155
|
+
with get_session() as session:
|
|
156
|
+
rows = (session.query(DocumentIndex)
|
|
157
|
+
.filter(DocumentIndex.corpus_name == corpus_name)
|
|
158
|
+
.order_by(DocumentIndex.indexed_at.desc()).all())
|
|
159
|
+
return [{"doc_id": r.doc_id, "filename": r.filename,
|
|
160
|
+
"multitoken_count": r.multitoken_count,
|
|
161
|
+
"page_count": r.page_count,
|
|
162
|
+
"indexed_at": r.indexed_at.isoformat() if r.indexed_at else ""}
|
|
163
|
+
for r in rows]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_all_document_texts(corpus_name: str = "default") -> list:
|
|
167
|
+
"""Retrieve all stored document texts for re-indexing on startup."""
|
|
168
|
+
with get_session() as session:
|
|
169
|
+
rows = (session.query(DocumentIndex)
|
|
170
|
+
.filter(DocumentIndex.corpus_name == corpus_name,
|
|
171
|
+
DocumentIndex.document_text.isnot(None),
|
|
172
|
+
DocumentIndex.document_text != "")
|
|
173
|
+
.all())
|
|
174
|
+
return [{"filename": r.filename, "text": r.document_text, "multitoken_count": r.multitoken_count}
|
|
175
|
+
for r in rows]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_indexed_documents(corpus_name: str = "default") -> List[Dict]:
|
|
179
|
+
with get_session() as session:
|
|
180
|
+
rows = (session.query(DocumentIndex)
|
|
181
|
+
.filter(DocumentIndex.corpus_name == corpus_name)
|
|
182
|
+
.order_by(desc(DocumentIndex.indexed_at)).all())
|
|
183
|
+
return [{"doc_id": r.doc_id, "filename": r.filename, "page_count": r.page_count,
|
|
184
|
+
"multitoken_count": r.multitoken_count,
|
|
185
|
+
"indexed_at": r.indexed_at.isoformat() if r.indexed_at else ""} for r in rows]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ── Agent Metrics ──────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
def record_agent_metric(agent_name: str, metric_name: str, metric_value: float) -> None:
|
|
191
|
+
with get_session() as session:
|
|
192
|
+
m = AgentMetrics(metric_id=str(uuid.uuid4()), agent_name=agent_name,
|
|
193
|
+
metric_name=metric_name, metric_value=metric_value, sample_count=1)
|
|
194
|
+
session.add(m)
|
|
195
|
+
session.commit()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_agent_metrics(agent_name: str, metric_name: str, limit: int = 100) -> List[Dict]:
|
|
199
|
+
with get_session() as session:
|
|
200
|
+
rows = (session.query(AgentMetrics)
|
|
201
|
+
.filter(AgentMetrics.agent_name == agent_name,
|
|
202
|
+
AgentMetrics.metric_name == metric_name)
|
|
203
|
+
.order_by(desc(AgentMetrics.computed_at)).limit(limit).all())
|
|
204
|
+
return [{"metric_value": r.metric_value, "computed_at": r.computed_at.isoformat() if r.computed_at else ""}
|
|
205
|
+
for r in rows]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ── Work Orders ─────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
def save_work_order(wo) -> str:
|
|
211
|
+
"""Save an extracted WorkOrder to the database. Returns work_order_id."""
|
|
212
|
+
import json
|
|
213
|
+
from backend.db.models import WorkOrderRecord
|
|
214
|
+
wo_id = str(uuid.uuid4())
|
|
215
|
+
with get_session() as session:
|
|
216
|
+
record = WorkOrderRecord(
|
|
217
|
+
wo_id=wo_id,
|
|
218
|
+
source_file=wo.source_file,
|
|
219
|
+
tail_number=wo.tail_number,
|
|
220
|
+
work_order_number=wo.work_order_number,
|
|
221
|
+
date=wo.date,
|
|
222
|
+
aircraft_model=wo.aircraft_model,
|
|
223
|
+
part_numbers=json.dumps(wo.part_numbers),
|
|
224
|
+
part_descriptions=json.dumps(wo.part_descriptions),
|
|
225
|
+
serial_numbers=json.dumps(wo.serial_numbers),
|
|
226
|
+
mechanic_id=wo.mechanic_id,
|
|
227
|
+
station=wo.station,
|
|
228
|
+
work_performed=wo.work_performed[:2000] if wo.work_performed else None,
|
|
229
|
+
hours_worked=wo.hours_worked,
|
|
230
|
+
ad_sb_references=json.dumps(wo.ad_sb_references),
|
|
231
|
+
inspector_stamp=wo.inspector_stamp,
|
|
232
|
+
extracted_fields_json=json.dumps([{
|
|
233
|
+
"field_name": f.field_name, "raw_label": f.raw_label,
|
|
234
|
+
"value": f.value, "confidence": f.confidence,
|
|
235
|
+
"page": f.page, "line": f.line_number,
|
|
236
|
+
} for f in wo.extracted_fields]),
|
|
237
|
+
)
|
|
238
|
+
session.add(record)
|
|
239
|
+
session.commit()
|
|
240
|
+
return wo_id
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def query_work_orders(tail_number: str = "", mechanic_id: str = "", limit: int = 50) -> list:
|
|
244
|
+
"""Query work orders by tail number, mechanic ID, or return recent."""
|
|
245
|
+
from backend.db.models import WorkOrderRecord
|
|
246
|
+
import json
|
|
247
|
+
with get_session() as session:
|
|
248
|
+
q = session.query(WorkOrderRecord)
|
|
249
|
+
if tail_number:
|
|
250
|
+
q = q.filter(WorkOrderRecord.tail_number == tail_number)
|
|
251
|
+
if mechanic_id:
|
|
252
|
+
q = q.filter(WorkOrderRecord.mechanic_id == mechanic_id)
|
|
253
|
+
rows = q.order_by(desc(WorkOrderRecord.created_at)).limit(limit).all()
|
|
254
|
+
return [{
|
|
255
|
+
"wo_id": r.wo_id,
|
|
256
|
+
"source_file": r.source_file,
|
|
257
|
+
"tail_number": r.tail_number,
|
|
258
|
+
"work_order_number": r.work_order_number,
|
|
259
|
+
"date": r.date,
|
|
260
|
+
"aircraft_model": r.aircraft_model,
|
|
261
|
+
"part_numbers": json.loads(r.part_numbers) if r.part_numbers else [],
|
|
262
|
+
"mechanic_id": r.mechanic_id,
|
|
263
|
+
"station": r.station,
|
|
264
|
+
"hours_worked": r.hours_worked,
|
|
265
|
+
"ad_sb_references": json.loads(r.ad_sb_references) if r.ad_sb_references else [],
|
|
266
|
+
"inspector_stamp": r.inspector_stamp,
|
|
267
|
+
"work_performed": (r.work_performed or "")[:300],
|
|
268
|
+
"created_at": r.created_at.isoformat() if r.created_at else "",
|
|
269
|
+
} for r in rows]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""© JINAN KORDAB — 2026 AI HYBRID AGENTIC RETRIEVAL-AUGMENTED GENERATION RAG PIPELINE - PERSONAL PROJECT"""
|
|
2
|
+
|
|
3
|
+
from typing import AsyncGenerator, List, Optional
|
|
4
|
+
from anthropic import AsyncAnthropic
|
|
5
|
+
from backend.llm.base import LLMProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AnthropicProvider(LLMProvider):
|
|
9
|
+
def __init__(self, api_key: str, model: str = "claude-3-5-sonnet-20241022") -> None:
|
|
10
|
+
self._client = AsyncAnthropic(api_key=api_key)
|
|
11
|
+
self._model = model
|
|
12
|
+
|
|
13
|
+
async def generate(self, prompt: str, system_prompt: Optional[str] = None,
|
|
14
|
+
temperature: float = 0.7, max_tokens: int = 4096, **kwargs) -> str:
|
|
15
|
+
resp = await self._client.messages.create(
|
|
16
|
+
model=self._model, max_tokens=max_tokens, temperature=temperature,
|
|
17
|
+
system=system_prompt or "",
|
|
18
|
+
messages=[{"role": "user", "content": prompt}], **kwargs)
|
|
19
|
+
return resp.content[0].text if resp.content else ""
|
|
20
|
+
|
|
21
|
+
async def generate_stream(self, prompt: str, system_prompt: Optional[str] = None,
|
|
22
|
+
temperature: float = 0.7, max_tokens: int = 4096, **kwargs) -> AsyncGenerator[str, None]:
|
|
23
|
+
async with self._client.messages.stream(
|
|
24
|
+
model=self._model, max_tokens=max_tokens, temperature=temperature,
|
|
25
|
+
system=system_prompt or "",
|
|
26
|
+
messages=[{"role": "user", "content": prompt}], **kwargs) as stream:
|
|
27
|
+
async for text in stream.text_stream:
|
|
28
|
+
yield text
|
|
29
|
+
|
|
30
|
+
async def embed(self, texts: List[str], model: Optional[str] = None) -> List[List[float]]:
|
|
31
|
+
raise NotImplementedError("Anthropic does not provide a public embeddings API")
|
|
32
|
+
|
|
33
|
+
def get_model_name(self) -> str: return self._model
|
|
34
|
+
|
|
35
|
+
def get_token_count(self, text: str) -> int:
|
|
36
|
+
try:
|
|
37
|
+
return self._client.count_tokens(text)
|
|
38
|
+
except Exception:
|
|
39
|
+
return len(text) // 4
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# © JINAN KORDAB — 2026 AI HYBRID AGENTIC RETRIEVAL-AUGMENTED GENERATION RAG PIPELINE - PERSONAL PROJECT
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Model-agnostic interface for all external LLM providers.
|
|
5
|
+
# Every provider implements this interface → orchestrator doesn't care which LLM.
|
|
6
|
+
#
|
|
7
|
+
# Supported providers:
|
|
8
|
+
# - OpenAIProvider → GPT-4o, GPT-4, GPT-3.5 (backend/llm/openai_provider.py)
|
|
9
|
+
# - AnthropicProvider → Claude 3.5 Sonnet, Claude 3 Opus
|
|
10
|
+
# - GoogleProvider → Gemini 1.5 Pro, Gemini 1.5 Flash
|
|
11
|
+
# - DeepSeekProvider → DeepSeek V4 Pro, DeepSeek Reasoner
|
|
12
|
+
# - OllamaProvider → Llama 3, Mistral, Phi-3 (local, no API key)
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# provider = LLMFactory.create("openai") # or "ollama" for local
|
|
16
|
+
# response = await provider.generate("Hello, world!")
|
|
17
|
+
#
|
|
18
|
+
# Related:
|
|
19
|
+
# backend/llm/factory.py — Factory for creating providers from config
|
|
20
|
+
# backend/config.py — Settings (API keys, default provider)
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from typing import List, Dict, Any, Optional, AsyncGenerator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LLMProvider(ABC):
|
|
28
|
+
"""
|
|
29
|
+
Abstract base for all LLM providers.
|
|
30
|
+
|
|
31
|
+
Every provider must implement:
|
|
32
|
+
- generate(): Single-turn text generation
|
|
33
|
+
- generate_stream(): Streaming text generation (for real-time UI)
|
|
34
|
+
- Embed (optional): Generate embeddings (for semantic fallback)
|
|
35
|
+
|
|
36
|
+
The orchestrator uses this interface exclusively — it never
|
|
37
|
+
imports provider-specific classes directly.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def generate(
|
|
42
|
+
self,
|
|
43
|
+
prompt: str,
|
|
44
|
+
system_prompt: Optional[str] = None,
|
|
45
|
+
temperature: float = 0.7,
|
|
46
|
+
max_tokens: int = 4096,
|
|
47
|
+
**kwargs,
|
|
48
|
+
) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Generate a text completion for the given prompt.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
prompt: The user/agent prompt
|
|
54
|
+
system_prompt: Optional system-level instruction
|
|
55
|
+
temperature: Creativity level (0.0 = deterministic, 1.0 = creative)
|
|
56
|
+
max_tokens: Maximum tokens in the response
|
|
57
|
+
**kwargs: Provider-specific parameters
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Generated text response
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
# TODO: Each provider implements this differently
|
|
64
|
+
# - OpenAI: client.chat.completions.create()
|
|
65
|
+
# - Anthropic: client.messages.create()
|
|
66
|
+
# - Google: model.generate_content()
|
|
67
|
+
# - Ollama: client.chat()
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
async def generate_stream(
|
|
72
|
+
self,
|
|
73
|
+
prompt: str,
|
|
74
|
+
system_prompt: Optional[str] = None,
|
|
75
|
+
temperature: float = 0.7,
|
|
76
|
+
max_tokens: int = 4096,
|
|
77
|
+
**kwargs,
|
|
78
|
+
) -> AsyncGenerator[str, None]:
|
|
79
|
+
"""
|
|
80
|
+
Stream a text completion token by token.
|
|
81
|
+
|
|
82
|
+
Used by the WebSocket endpoint to stream agent outputs
|
|
83
|
+
to the frontend in real-time (AgentTimeline component).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
prompt: The user/agent prompt
|
|
87
|
+
system_prompt: Optional system-level instruction
|
|
88
|
+
temperature: Creativity level
|
|
89
|
+
max_tokens: Maximum tokens
|
|
90
|
+
|
|
91
|
+
Yields:
|
|
92
|
+
Text tokens as they are generated
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
95
|
+
# TODO: Each provider implements streaming differently
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def embed(
|
|
100
|
+
self,
|
|
101
|
+
texts: List[str],
|
|
102
|
+
model: Optional[str] = None,
|
|
103
|
+
) -> List[List[float]]:
|
|
104
|
+
"""
|
|
105
|
+
Generate embeddings for a list of texts.
|
|
106
|
+
|
|
107
|
+
Used ONLY for semantic fallback search — not the primary retrieval path.
|
|
108
|
+
Primary retrieval uses ExactHash (96% accurate, no embeddings needed).
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
texts: List of texts to embed
|
|
112
|
+
model: Optional model override
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of embedding vectors
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def get_model_name(self) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Return the currently active model name.
|
|
124
|
+
|
|
125
|
+
Used for logging, cost tracking, and UI display.
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def get_token_count(self, text: str) -> int:
|
|
132
|
+
"""
|
|
133
|
+
Estimate token count for a text string.
|
|
134
|
+
|
|
135
|
+
Used for:
|
|
136
|
+
- Budget tracking (cost estimation before API call)
|
|
137
|
+
- Context window management (don't exceed model limits)
|
|
138
|
+
- Prompt optimization (trim if needed)
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
text: The text to count tokens for
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Estimated token count
|
|
145
|
+
"""
|
|
146
|
+
...
|
|
147
|
+
pass
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""© JINAN KORDAB — 2026 AI HYBRID AGENTIC RETRIEVAL-AUGMENTED GENERATION RAG PIPELINE - PERSONAL PROJECT"""
|
|
2
|
+
|
|
3
|
+
from typing import AsyncGenerator, List, Optional
|
|
4
|
+
from openai import AsyncOpenAI
|
|
5
|
+
from backend.llm.base import LLMProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeepSeekProvider(LLMProvider):
|
|
9
|
+
def __init__(self, api_key: str, model: str = "deepseek-chat",
|
|
10
|
+
base_url: str = "https://api.deepseek.com") -> None:
|
|
11
|
+
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
12
|
+
self._model = model
|
|
13
|
+
|
|
14
|
+
async def generate(self, prompt: str, system_prompt: Optional[str] = None,
|
|
15
|
+
temperature: float = 0.7, max_tokens: int = 4096, **kwargs) -> str:
|
|
16
|
+
messages = []
|
|
17
|
+
if system_prompt:
|
|
18
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
19
|
+
messages.append({"role": "user", "content": prompt})
|
|
20
|
+
resp = await self._client.chat.completions.create(
|
|
21
|
+
model=self._model, messages=messages, temperature=temperature,
|
|
22
|
+
max_tokens=max_tokens, timeout=30, **kwargs)
|
|
23
|
+
return resp.choices[0].message.content or ""
|
|
24
|
+
|
|
25
|
+
async def generate_stream(self, prompt: str, system_prompt: Optional[str] = None,
|
|
26
|
+
temperature: float = 0.7, max_tokens: int = 4096, **kwargs) -> AsyncGenerator[str, None]:
|
|
27
|
+
messages = []
|
|
28
|
+
if system_prompt:
|
|
29
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
30
|
+
messages.append({"role": "user", "content": prompt})
|
|
31
|
+
stream = await self._client.chat.completions.create(
|
|
32
|
+
model=self._model, messages=messages, temperature=temperature,
|
|
33
|
+
max_tokens=max_tokens, stream=True, **kwargs)
|
|
34
|
+
async for chunk in stream:
|
|
35
|
+
if chunk.choices[0].delta.content:
|
|
36
|
+
yield chunk.choices[0].delta.content
|
|
37
|
+
|
|
38
|
+
async def embed(self, texts: List[str], model: Optional[str] = None) -> List[List[float]]:
|
|
39
|
+
raise NotImplementedError("DeepSeek embeddings — check platform.deepseek.com for availability")
|
|
40
|
+
|
|
41
|
+
def get_model_name(self) -> str: return self._model
|
|
42
|
+
|
|
43
|
+
def get_token_count(self, text: str) -> int: return len(text) // 4
|