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.
Files changed (119) hide show
  1. package/.env.example +93 -0
  2. package/README.md +258 -0
  3. package/package.json +70 -0
  4. package/packages/clinical/package.json +22 -0
  5. package/packages/clinical/src/index.ts +262 -0
  6. package/packages/clinical/tsconfig.json +13 -0
  7. package/packages/core/package.json +21 -0
  8. package/packages/core/src/config.ts +138 -0
  9. package/packages/core/src/errors.ts +100 -0
  10. package/packages/core/src/index.ts +104 -0
  11. package/packages/core/src/llm-config.ts +213 -0
  12. package/packages/core/src/logging.ts +66 -0
  13. package/packages/core/src/python-bridge.ts +384 -0
  14. package/packages/core/src/rate-limiter.ts +136 -0
  15. package/packages/core/src/types.ts +203 -0
  16. package/packages/core/src/validation.ts +101 -0
  17. package/packages/core/tsconfig.json +10 -0
  18. package/packages/deeppipe/package.json +21 -0
  19. package/packages/deeppipe/src/index.ts +424 -0
  20. package/packages/deeppipe/tsconfig.json +13 -0
  21. package/packages/piste/package.json +20 -0
  22. package/packages/piste/src/index.ts +48 -0
  23. package/packages/piste/tsconfig.json +13 -0
  24. package/packages/precis/package.json +20 -0
  25. package/packages/precis/src/index.ts +67 -0
  26. package/packages/precis/tsconfig.json +13 -0
  27. package/packages/server/package.json +31 -0
  28. package/packages/server/src/index.ts +427 -0
  29. package/packages/server/tsconfig.json +17 -0
  30. package/setup.mjs +141 -0
  31. package/test.mjs +337 -0
  32. package/vendors/clinical-intake/pipeline.mjs +349 -0
  33. package/vendors/clinical-intake/questions/en.txt +9 -0
  34. package/vendors/clinical-intake/questions/fr.txt +9 -0
  35. package/vendors/piste/.env.example +73 -0
  36. package/vendors/piste/app/core/__init__.py +4 -0
  37. package/vendors/piste/app/core/config.py +83 -0
  38. package/vendors/piste/app/core/debuglog.py +16 -0
  39. package/vendors/piste/app/core/middleware.py +40 -0
  40. package/vendors/piste/bridge_piste.py +301 -0
  41. package/vendors/piste/pipeline/__init__.py +4 -0
  42. package/vendors/piste/pipeline/compiler.py +68 -0
  43. package/vendors/piste/pipeline/offline/__init__.py +28 -0
  44. package/vendors/piste/pipeline/offline/verifaid_pipeline.py +247 -0
  45. package/vendors/piste/pipeline/replay.py +15 -0
  46. package/vendors/piste/pipeline/replay_engine.py +249 -0
  47. package/vendors/piste/pipeline/signatures/__init__.py +4 -0
  48. package/vendors/piste/pipeline/signatures/signatures.py +136 -0
  49. package/vendors/piste/pipeline/stage1/__init__.py +21 -0
  50. package/vendors/piste/pipeline/stage1/atomic_decomposer.py +61 -0
  51. package/vendors/piste/pipeline/stage1/check_worthiness.py +100 -0
  52. package/vendors/piste/pipeline/stage1/orchestrator.py +175 -0
  53. package/vendors/piste/pipeline/stage1/test_stage1.py +162 -0
  54. package/vendors/piste/pipeline/stage2/__init__.py +34 -0
  55. package/vendors/piste/pipeline/stage2/blind_retriever.py +303 -0
  56. package/vendors/piste/pipeline/stage2/canonical_mapper.py +124 -0
  57. package/vendors/piste/pipeline/stage2/credibility_scorer.py +85 -0
  58. package/vendors/piste/pipeline/stage2/orchestrator.py +311 -0
  59. package/vendors/piste/pipeline/stage2/query_refiner.py +88 -0
  60. package/vendors/piste/pipeline/stage2/search_decision.py +69 -0
  61. package/vendors/piste/pipeline/stage2/test_stage2.py +265 -0
  62. package/vendors/piste/pipeline/stage3/__init__.py +20 -0
  63. package/vendors/piste/pipeline/stage3/classifier.py +79 -0
  64. package/vendors/piste/pipeline/stage3/orchestrator.py +225 -0
  65. package/vendors/piste/pipeline/stage3/test_stage3.py +101 -0
  66. package/vendors/piste/pipeline/stage4/__init__.py +33 -0
  67. package/vendors/piste/pipeline/stage4/criticality_gate.py +177 -0
  68. package/vendors/piste/pipeline/stage4/orchestrator.py +269 -0
  69. package/vendors/piste/pipeline/stage4/test_stage4.py +192 -0
  70. package/vendors/piste/pipeline/stage4/verdict_aggregator.py +157 -0
  71. package/vendors/piste/requirements.txt +53 -0
  72. package/vendors/precis/backend/__init__.py +6 -0
  73. package/vendors/precis/backend/agents/__init__.py +3 -0
  74. package/vendors/precis/backend/agents/data_synthesis.py +105 -0
  75. package/vendors/precis/backend/agents/dist_free_synth.py +97 -0
  76. package/vendors/precis/backend/agents/exact_hash_retriever.py +327 -0
  77. package/vendors/precis/backend/agents/fusion_ranker.py +64 -0
  78. package/vendors/precis/backend/agents/guardrail.py +175 -0
  79. package/vendors/precis/backend/agents/query_expander.py +89 -0
  80. package/vendors/precis/backend/agents/radial_interpol.py +99 -0
  81. package/vendors/precis/backend/agents/report_generator.py +92 -0
  82. package/vendors/precis/backend/agents/semantic_reranker.py +135 -0
  83. package/vendors/precis/backend/agents/stat_anomaly.py +93 -0
  84. package/vendors/precis/backend/agents/vector_index.py +123 -0
  85. package/vendors/precis/backend/agents/veri_score.py +341 -0
  86. package/vendors/precis/backend/agents/work_order_extractor.py +205 -0
  87. package/vendors/precis/backend/api/__init__.py +3 -0
  88. package/vendors/precis/backend/api/routes/__init__.py +3 -0
  89. package/vendors/precis/backend/config.py +88 -0
  90. package/vendors/precis/backend/core/__init__.py +13 -0
  91. package/vendors/precis/backend/core/hashing.py +22 -0
  92. package/vendors/precis/backend/core/metrics.py +77 -0
  93. package/vendors/precis/backend/core/multitoken.py +166 -0
  94. package/vendors/precis/backend/core/pmi.py +54 -0
  95. package/vendors/precis/backend/core/stemming.py +74 -0
  96. package/vendors/precis/backend/core/tracing.py +150 -0
  97. package/vendors/precis/backend/data/__init__.py +3 -0
  98. package/vendors/precis/backend/data/chunker.py +57 -0
  99. package/vendors/precis/backend/data/pdf_parser.py +42 -0
  100. package/vendors/precis/backend/db/__init__.py +3 -0
  101. package/vendors/precis/backend/db/models.py +173 -0
  102. package/vendors/precis/backend/db/repository.py +269 -0
  103. package/vendors/precis/backend/llm/__init__.py +3 -0
  104. package/vendors/precis/backend/llm/anthropic_provider.py +39 -0
  105. package/vendors/precis/backend/llm/base.py +147 -0
  106. package/vendors/precis/backend/llm/deepseek_provider.py +43 -0
  107. package/vendors/precis/backend/llm/factory.py +60 -0
  108. package/vendors/precis/backend/llm/google_provider.py +39 -0
  109. package/vendors/precis/backend/llm/ollama_provider.py +54 -0
  110. package/vendors/precis/backend/llm/openai_provider.py +50 -0
  111. package/vendors/precis/backend/main.py +677 -0
  112. package/vendors/precis/backend/orchestrator/__init__.py +3 -0
  113. package/vendors/precis/backend/orchestrator/planner.py +81 -0
  114. package/vendors/precis/backend/orchestrator/router.py +319 -0
  115. package/vendors/precis/backend/orchestrator/types.py +58 -0
  116. package/vendors/precis/bridge_precis.py +185 -0
  117. package/vendors/precis/data/sample_reports/README.md +8 -0
  118. package/vendors/precis/data/seed_data.py +115 -0
  119. 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,3 @@
1
+ # =============================================================================
2
+ # © JINAN KORDAB — 2026 AI HYBRID AGENTIC RETRIEVAL-AUGMENTED GENERATION RAG PIPELINE - PERSONAL PROJECT
3
+ # =============================================================================
@@ -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