ALPHA0008 commited on
Commit
0762fba
·
0 Parent(s):

feat: initial commit - core multi-agent compiler engine and frontend UI

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +42 -0
  2. CLAUDE.md +444 -0
  3. backend/.env.example +4 -0
  4. backend/agent/brain_agent.py +144 -0
  5. backend/db/schema.sql +58 -0
  6. backend/db/supabase.py +63 -0
  7. backend/graph/graph.py +30 -0
  8. backend/graph/nodes/cluster_evidence.py +64 -0
  9. backend/graph/nodes/load_and_chunk.py +174 -0
  10. backend/graph/nodes/quality_normalize.py +83 -0
  11. backend/graph/nodes/synthesize_skills.py +111 -0
  12. backend/graph/nodes/write_brain.py +96 -0
  13. backend/graph/state.py +14 -0
  14. backend/llm.py +65 -0
  15. backend/main.py +310 -0
  16. backend/models/schemas.py +20 -0
  17. backend/requirements.txt +10 -0
  18. backend/sse.py +40 -0
  19. backend/test_compile.py +89 -0
  20. brand_alchemy_company_brain.html +254 -0
  21. company_brain_PRD_v4.md +1061 -0
  22. data/sources/rivanly-inc/notion_cs_playbook.md +10 -0
  23. data/sources/rivanly-inc/notion_eng_runbook.md +17 -0
  24. data/sources/rivanly-inc/notion_hr_playbook.md +17 -0
  25. data/sources/rivanly-inc/notion_pricing_policy.md +14 -0
  26. data/sources/rivanly-inc/notion_refund_sop.md +16 -0
  27. data/sources/rivanly-inc/slack_export_ops.json +20 -0
  28. data/sources/rivanly-inc/slack_export_support.json +26 -0
  29. data/sources/rivanly-inc/zendesk_tickets.json +23 -0
  30. frontend/.gitignore +41 -0
  31. frontend/AGENTS.md +5 -0
  32. frontend/CLAUDE.md +1 -0
  33. frontend/README.md +36 -0
  34. frontend/eslint.config.mjs +18 -0
  35. frontend/next.config.ts +7 -0
  36. frontend/package-lock.json +0 -0
  37. frontend/package.json +26 -0
  38. frontend/postcss.config.mjs +7 -0
  39. frontend/public/file.svg +1 -0
  40. frontend/public/globe.svg +1 -0
  41. frontend/public/next.svg +1 -0
  42. frontend/public/vercel.svg +1 -0
  43. frontend/public/window.svg +1 -0
  44. frontend/src/app/compile/[jobId]/page.tsx +115 -0
  45. frontend/src/app/demo/[companyId]/page.tsx +269 -0
  46. frontend/src/app/favicon.ico +0 -0
  47. frontend/src/app/globals.css +24 -0
  48. frontend/src/app/layout.tsx +33 -0
  49. frontend/src/app/page.tsx +90 -0
  50. frontend/src/app/skills/[companyId]/page.tsx +162 -0
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # OS files
7
+ .DS_Store
8
+ Thumbs.db
9
+
10
+ # Environments & Secret credentials
11
+ .env
12
+ .env.local
13
+ .env*.local
14
+ backend/.env
15
+ frontend/.env
16
+
17
+ # Local database files
18
+ *.db
19
+ *.sqlite
20
+ *.sqlite3
21
+ *.sqlite-journal
22
+ backend/db/local_brain.db
23
+
24
+ # Node dependencies & Next.js cache
25
+ node_modules/
26
+ .next/
27
+ out/
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # Unit test & Coverage reports
34
+ .pytest_cache/
35
+ .coverage
36
+ htmlcov/
37
+ .cache/
38
+
39
+ # User data sources (ignore uploaded dynamic sources, keep demo ones if needed)
40
+ # For the hackathon, we keep the static demo rivanly-inc files, but ignore other companies if uploaded
41
+ data/sources/*/
42
+ !data/sources/rivanly-inc/
CLAUDE.md ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Company Brain — CLAUDE.md
2
+ ## Project context for AI coding assistants
3
+
4
+ ---
5
+
6
+ ## What This Project Is
7
+
8
+ Company Brain is a multi-agent compilation pipeline that extracts operational decision knowledge from company data sources (Slack, Notion SOPs, support tickets) and compiles it into a versioned, evidence-linked, executable skills file. A downstream brain agent uses this skills file to handle operational scenarios correctly — acting like the company's best employee.
9
+
10
+ **The core thesis:** Agents are compilers, not assistants. We don't search raw documents. We compile tribal knowledge into structured, executable logic once. Then we read the compiled output forever.
11
+
12
+ ---
13
+
14
+ ## Monorepo Structure
15
+
16
+ ```
17
+ company-brain/
18
+ ├── backend/ ← FastAPI + LangGraph pipeline (Python)
19
+ │ ├── main.py ← FastAPI app entry point
20
+ │ ├── graph/
21
+ │ │ ├── state.py ← BrainState TypedDict
22
+ │ │ ├── nodes/ ← one file per LangGraph node
23
+ │ │ │ ├── ingest_slack.py
24
+ │ │ │ ├── ingest_notion.py
25
+ │ │ │ ├── ingest_tickets.py
26
+ │ │ │ ├── ingest_join.py
27
+ │ │ │ ├── extract_decisions.py
28
+ │ │ │ ├── extract_workflows.py
29
+ │ │ │ ├── extract_exceptions.py
30
+ │ │ │ ├── detect_contradictions.py
31
+ │ │ │ ├── synthesize_skills.py
32
+ │ │ │ ├── link_evidence.py
33
+ │ │ │ ├── score_confidence.py
34
+ │ │ │ └── write_brain.py
35
+ │ │ └── graph.py ← graph assembly + compile
36
+ │ ├── agents/
37
+ │ │ └── brain_agent.py ← query-time brain agent
38
+ │ ├── db/
39
+ │ │ └── supabase.py ← Supabase client + queries
40
+ │ ├── models/
41
+ │ │ └── schemas.py ← Pydantic models for API
42
+ │ └── requirements.txt
43
+ ├── frontend/ ← Next.js 14 + Tailwind (Harshit)
44
+ ├── data/
45
+ │ └── sources/ ← 8 synthetic source files
46
+ │ ├── notion_refund_sop.md
47
+ │ ├── notion_pricing_policy.md
48
+ │ ├── notion_eng_runbook.md
49
+ │ ├── notion_hr_playbook.md
50
+ │ ├── notion_cs_playbook.md
51
+ │ ├── slack_export_support.json
52
+ │ ├── slack_export_ops.json
53
+ │ └── zendesk_tickets.json
54
+ └── CLAUDE.md ← this file
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Tech Stack
60
+
61
+ | Layer | Technology |
62
+ |---|---|
63
+ | LLM inference | vLLM serving `RedHatAI/Qwen2.5-72B-Instruct-FP8-dynamic` on AMD MI300X, port 8000 |
64
+ | LLM client | `openai` Python SDK pointed at `http://localhost:8000/v1` |
65
+ | Agent orchestration | `langgraph` with async nodes + `Send` API for parallel fan-out |
66
+ | State checkpointing | `MemorySaver` (in-memory for v0) |
67
+ | Embedding (skill matching) | `sentence-transformers` `all-MiniLM-L6-v2` in-memory, CPU |
68
+ | Web framework | `FastAPI` with `uvicorn` |
69
+ | Real-time streaming | FastAPI `StreamingResponse` with `text/event-stream` |
70
+ | Database | Supabase (Postgres) via `supabase-py` |
71
+ | File storage | Supabase Storage |
72
+
73
+ ---
74
+
75
+ ## LLM Client Setup
76
+
77
+ ```python
78
+ from openai import AsyncOpenAI
79
+
80
+ llm = AsyncOpenAI(
81
+ base_url="http://localhost:8000/v1",
82
+ api_key="not-needed"
83
+ )
84
+
85
+ # All LLM calls use this pattern:
86
+ response = await llm.chat.completions.create(
87
+ model="RedHatAI/Qwen2.5-72B-Instruct-FP8-dynamic",
88
+ messages=[
89
+ {"role": "system", "content": system_prompt},
90
+ {"role": "user", "content": user_content}
91
+ ],
92
+ temperature=0.1,
93
+ max_tokens=4096
94
+ )
95
+ result = response.choices[0].message.content
96
+ ```
97
+
98
+ **Never use `openai.OpenAI()` — always use `AsyncOpenAI`. All nodes are async.**
99
+
100
+ ---
101
+
102
+ ## BrainState — The Central Data Structure
103
+
104
+ ```python
105
+ from typing import TypedDict, Annotated
106
+ import operator
107
+
108
+ class BrainState(TypedDict):
109
+ company_id: str
110
+ source_files: list[dict] # [{filename, content, sha256, type}]
111
+
112
+ # Ingestion outputs (parallel, accumulated with operator.add)
113
+ normalized_events: Annotated[list[dict], operator.add] # from Slack
114
+ structured_sops: Annotated[list[dict], operator.add] # from Notion
115
+ resolved_cases: Annotated[list[dict], operator.add] # from tickets
116
+
117
+ # Extraction outputs (parallel, accumulated with operator.add)
118
+ raw_decisions: Annotated[list[dict], operator.add]
119
+ workflow_steps: Annotated[list[dict], operator.add]
120
+ exception_rules: Annotated[list[dict], operator.add]
121
+ contradictions: Annotated[list[dict], operator.add]
122
+
123
+ # Compilation outputs (sequential)
124
+ draft_skills: list[dict]
125
+ skills_with_evidence: list[dict]
126
+ final_skills: list[dict]
127
+
128
+ # Metadata
129
+ job_id: str
130
+ brain_version: str
131
+ errors: Annotated[list[str], operator.add]
132
+ ```
133
+
134
+ **The `Annotated[list, operator.add]` pattern is critical.** It allows multiple parallel nodes to write to the same list field without overwriting each other. Do not change this.
135
+
136
+ ---
137
+
138
+ ## LangGraph Architecture — Fan-Out Pattern
139
+
140
+ ```python
141
+ from langgraph.graph import StateGraph
142
+ from langgraph.checkpoint.memory import MemorySaver
143
+ from langgraph.types import Send
144
+
145
+ def route_to_ingestion(state: BrainState) -> list[Send]:
146
+ """Fan out to 3 parallel ingestion nodes based on source file types."""
147
+ sends = []
148
+ for file in state["source_files"]:
149
+ if file["type"] == "slack_json":
150
+ sends.append(Send("ingest_slack", {"source_files": [file], ...}))
151
+ elif file["type"] == "notion_md":
152
+ sends.append(Send("ingest_notion", {"source_files": [file], ...}))
153
+ elif file["type"] == "tickets_json":
154
+ sends.append(Send("ingest_tickets", {"source_files": [file], ...}))
155
+ return sends
156
+
157
+ def route_to_extraction(state: BrainState) -> list[Send]:
158
+ """Fan out to 4 parallel extraction nodes after ingestion join."""
159
+ return [
160
+ Send("extract_decisions", state),
161
+ Send("extract_workflows", state),
162
+ Send("extract_exceptions", state),
163
+ Send("detect_contradictions", state),
164
+ ]
165
+
166
+ # Graph assembly:
167
+ # START → route_to_ingestion (conditional) → [ingest_slack, ingest_notion, ingest_tickets]
168
+ # → ingest_join (barrier, waits for all) → route_to_extraction (conditional)
169
+ # → [extract_decisions, extract_workflows, extract_exceptions, detect_contradictions]
170
+ # → synthesize_skills → link_evidence → score_confidence → write_brain → END
171
+ ```
172
+
173
+ **Never use `graph.add_edge("extractor", "synthesize_skills")` for parallel nodes — this causes synthesize_skills to fire multiple times. Always use the `Send` API + barrier join node.**
174
+
175
+ ---
176
+
177
+ ## Extraction Prompt Pattern
178
+
179
+ Every extraction node uses this prompt structure:
180
+
181
+ ```python
182
+ SYSTEM = """You are a policy analyst. Your ONLY job is to extract {type} from company communications.
183
+ Output ONLY a JSON array. No preamble. No explanation. No markdown.
184
+ Each item must have exactly these fields: {schema}
185
+ If you find nothing, output: []
186
+ Example output: {example}"""
187
+
188
+ USER = """Extract all {type} from this company data:
189
+ {content}"""
190
+ ```
191
+
192
+ - Temperature: always `0.1`
193
+ - Max tokens: `4096`
194
+ - Always wrap LLM call in try/except — on JSON parse failure, retry once with stricter prompt, then return `[]`
195
+
196
+ ---
197
+
198
+ ## Skills File Schema (per skill)
199
+
200
+ ```python
201
+ {
202
+ "id": "handle_refund_request", # snake_case
203
+ "name": "Handle Refund Request", # human readable
204
+ "domain": "support", # support|revenue|product_eng|customer_success|hr|finance_ops
205
+ "version": "1.0",
206
+ "confidence": 0.91, # 0.0 - 1.0
207
+ "stale": False,
208
+ "review_required": False, # True if confidence < 0.6
209
+ "last_updated": "2026-05-04T09:30:00Z",
210
+ "trigger": {
211
+ "phrases": ["refund", "money back"],
212
+ "conditions": ["customer mentions payment dissatisfaction"]
213
+ },
214
+ "decision_logic": [
215
+ {
216
+ "condition": "plan == 'annual' AND days_since_purchase <= 14",
217
+ "action": "approve_full_refund",
218
+ "note": "No-questions policy within 14 days.",
219
+ "evidence_sources": [
220
+ {
221
+ "source": "notion_refund_sop.md",
222
+ "excerpt": "Annual plan customers within 14 days...",
223
+ "confidence": 0.95
224
+ }
225
+ ]
226
+ }
227
+ ],
228
+ "forbidden_actions": [
229
+ "Never process refunds for lifetime deal accounts"
230
+ ],
231
+ "escalation_chain": ["support_agent", "support_lead", "account_manager", "founder"],
232
+ "sla": "respond_within_2h, resolve_within_24h"
233
+ }
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Confidence Scoring Formula
239
+
240
+ ```python
241
+ def score_confidence(skill: dict, all_sources: list[dict]) -> float:
242
+ base = 0.5
243
+
244
+ # More sources = higher confidence
245
+ source_count = len(skill["decision_logic"][0].get("evidence_sources", []))
246
+ if source_count >= 3:
247
+ base += 0.25
248
+ elif source_count == 2:
249
+ base += 0.15
250
+ elif source_count == 1:
251
+ base += 0.05
252
+
253
+ # Recent sources = higher confidence
254
+ # (check source file last_modified if available)
255
+ base += 0.15 # assume recent for v0
256
+
257
+ # No contradictions for this skill = higher confidence
258
+ # (passed in from contradiction detector)
259
+ has_contradiction = False # check contradictions list
260
+ if not has_contradiction:
261
+ base += 0.10
262
+
263
+ return min(base, 1.0)
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Brain Agent Pattern
269
+
270
+ ```python
271
+ from sentence_transformers import SentenceTransformer
272
+ import numpy as np
273
+
274
+ # Load once at startup
275
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
276
+
277
+ # Pre-compute skill embeddings (call after compile)
278
+ skill_embeddings = {} # {skill_id: np.array}
279
+
280
+ def compute_skill_embeddings(skills: list[dict]):
281
+ global skill_embeddings
282
+ for skill in skills:
283
+ text = f"{skill['name']} {' '.join(skill['trigger']['phrases'])}"
284
+ skill_embeddings[skill['id']] = embedder.encode(text)
285
+
286
+ def match_skill(query: str) -> tuple[str, float]:
287
+ query_emb = embedder.encode(query)
288
+ scores = {}
289
+ for skill_id, emb in skill_embeddings.items():
290
+ score = float(np.dot(query_emb, emb) /
291
+ (np.linalg.norm(query_emb) * np.linalg.norm(emb)))
292
+ scores[skill_id] = score
293
+ best_id = max(scores, key=scores.get)
294
+ return best_id, scores[best_id]
295
+
296
+ def skill_to_markdown(skill: dict) -> str:
297
+ """Convert skill JSON to markdown for prompt injection."""
298
+ lines = [f"## {skill['name']}", ""]
299
+ for logic in skill['decision_logic']:
300
+ lines.append(f"- IF {logic['condition']}: {logic['action']}")
301
+ if logic.get('note'):
302
+ lines.append(f" Note: {logic['note']}")
303
+ lines.append("")
304
+ lines.append("FORBIDDEN: " + "; ".join(skill['forbidden_actions']))
305
+ lines.append("ESCALATE: " + " → ".join(skill['escalation_chain']))
306
+ return "\n".join(lines)
307
+ ```
308
+
309
+ ---
310
+
311
+ ## FastAPI SSE Pattern
312
+
313
+ ```python
314
+ from fastapi import FastAPI
315
+ from fastapi.responses import StreamingResponse
316
+ import asyncio
317
+ import json
318
+
319
+ async def event_generator(job_id: str):
320
+ """Yields SSE events during compilation."""
321
+ async for event in compilation_events[job_id]:
322
+ yield f"event: {event['type']}\ndata: {json.dumps(event['data'])}\n\n"
323
+
324
+ @app.get("/compile/stream")
325
+ async def stream_compile(job_id: str):
326
+ return StreamingResponse(
327
+ event_generator(job_id),
328
+ media_type="text/event-stream",
329
+ headers={
330
+ "Cache-Control": "no-cache",
331
+ "Connection": "keep-alive",
332
+ "Access-Control-Allow-Origin": "*" # CORS for frontend
333
+ }
334
+ )
335
+ ```
336
+
337
+ ---
338
+
339
+ ## Supabase Tables
340
+
341
+ ```sql
342
+ -- Run these in Supabase SQL editor before starting
343
+
344
+ CREATE TABLE companies (
345
+ id TEXT PRIMARY KEY,
346
+ name TEXT NOT NULL,
347
+ created_at TIMESTAMPTZ DEFAULT now()
348
+ );
349
+
350
+ INSERT INTO companies VALUES ('rivanly-inc', 'Rivanly Inc.', now());
351
+
352
+ CREATE TABLE skills_files (
353
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
354
+ company_id TEXT REFERENCES companies(id),
355
+ version TEXT NOT NULL,
356
+ brain_json JSONB NOT NULL,
357
+ source_hashes JSONB NOT NULL,
358
+ compiled_at TIMESTAMPTZ DEFAULT now(),
359
+ is_current BOOLEAN DEFAULT false
360
+ );
361
+
362
+ CREATE UNIQUE INDEX idx_one_current_per_company
363
+ ON skills_files(company_id) WHERE is_current = true;
364
+
365
+ CREATE TABLE compile_runs (
366
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
367
+ company_id TEXT REFERENCES companies(id),
368
+ status TEXT CHECK (status IN ('started','running','complete','error')),
369
+ started_at TIMESTAMPTZ DEFAULT now(),
370
+ completed_at TIMESTAMPTZ,
371
+ duration_ms INTEGER,
372
+ result_version TEXT,
373
+ error_detail TEXT
374
+ );
375
+
376
+ CREATE TABLE source_files (
377
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
378
+ company_id TEXT REFERENCES companies(id),
379
+ filename TEXT NOT NULL,
380
+ sha256 TEXT NOT NULL,
381
+ content TEXT NOT NULL,
382
+ source_type TEXT CHECK (source_type IN ('slack_json','notion_md','tickets_json')),
383
+ uploaded_at TIMESTAMPTZ DEFAULT now()
384
+ );
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Environment Variables
390
+
391
+ ```bash
392
+ # backend/.env
393
+ VLLM_BASE_URL=http://localhost:8000/v1
394
+ SUPABASE_URL=your_supabase_project_url
395
+ SUPABASE_KEY=your_supabase_anon_key
396
+ COMPANY_ID=rivanly-inc
397
+ ```
398
+
399
+ ---
400
+
401
+ ## API Endpoints — Full List
402
+
403
+ ```
404
+ POST /compile → trigger pipeline, returns {job_id, stream_url}
405
+ GET /compile/stream → SSE stream for job_id
406
+ GET /brain/status → current brain version + stats
407
+ GET /skills → all skills (lightweight)
408
+ GET /skills/{id} → full skill detail
409
+ POST /agent/handle → brain agent query
410
+ GET /diff/{v1}/{v2} → version diff
411
+ POST /sources/upload → upload source files
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Critical Rules — Do Not Violate
417
+
418
+ 1. **All LangGraph nodes must be `async def`** — sync nodes break parallelism
419
+ 2. **Use `Send` API for fan-out, never direct edges between parallel nodes and their join**
420
+ 3. **Never read raw source files at query time** — brain agent reads skills file only
421
+ 4. **All LLM calls wrapped in try/except** — retry once on JSON parse failure, return `[]` if still failing
422
+ 5. **`skills_files.is_current` enforced by partial unique index** — only one current per company
423
+ 6. **`compile_runs` table is append-only** — never update rows, only insert
424
+ 7. **CORS headers on all endpoints** — frontend is on different domain
425
+ 8. **Temperature 0.1 on all extraction calls** — deterministic is better than creative here
426
+
427
+ ---
428
+
429
+ ## Demo Company — Rivanly Inc.
430
+
431
+ The demo uses Rivanly Inc. — a fictional 15-person B2B SaaS company.
432
+
433
+ 6 departments, 12 skills:
434
+
435
+ | Department | Skills |
436
+ |---|---|
437
+ | Support | handle_refund_request, respond_to_outage |
438
+ | Revenue | handle_pricing_exception, evaluate_discount_request |
439
+ | Product/Eng | prioritize_bug_report, handle_sla_breach |
440
+ | Customer Success | evaluate_churn_risk, enterprise_onboarding_steps |
441
+ | HR | hiring_process_engineering, performance_pip_trigger |
442
+ | Finance | approve_vendor_payment, expense_policy_exception |
443
+
444
+ The 8 synthetic source files in `data/sources/` are authored to produce these 12 skills when processed by the pipeline.
backend/.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ VLLM_BASE_URL=http://<MI300X_IP>:8000/v1
2
+ SUPABASE_URL=your_supabase_project_url
3
+ SUPABASE_KEY=your_supabase_anon_key
4
+ COMPANY_ID=rivanly-inc
backend/agent/brain_agent.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from backend.db.supabase import get_client
3
+ from backend.llm import llm_call, get_embedding, cosine_similarity
4
+
5
+
6
+ async def handle_agent_query(company_id: str, scenario: str, context: dict = None, with_brain: bool = True) -> dict:
7
+ """
8
+ Real agent query handler. No keyword routing, no hardcoded actions.
9
+ Everything flows through: retrieve skills -> build prompt -> call vLLM -> return raw result.
10
+ """
11
+ if not with_brain:
12
+ return await _baseline_query(scenario, context)
13
+
14
+ # --- WITH BRAIN ---
15
+ db = get_client()
16
+ if not db:
17
+ return _error_response("Database connection failed.")
18
+
19
+ # 1. Fetch latest compiled skills
20
+ res = db.table("skills_files").select("brain_json").eq(
21
+ "company_id", company_id
22
+ ).order("compiled_at", desc=True).limit(1).execute()
23
+
24
+ if not res.data:
25
+ return _error_response("No compiled brain found. Please compile first.")
26
+
27
+ skills = res.data[0]["brain_json"].get("skills", [])
28
+ if not skills:
29
+ return _error_response("Brain is empty — no skills compiled.")
30
+
31
+ # 2. Embed the query and score every skill
32
+ query_text = f"{scenario} {json.dumps(context or {})}"
33
+ query_emb = get_embedding(query_text)
34
+
35
+ scored = []
36
+ for i, skill in enumerate(skills):
37
+ skill_text = f"{skill.get('category', '')} {skill.get('rule', '')} {skill.get('rationale', '')}"
38
+ skill_emb = get_embedding(skill_text)
39
+ score = cosine_similarity(query_emb, skill_emb)
40
+ scored.append({"skill": skill, "score": round(score, 4), "index": i})
41
+
42
+ scored.sort(key=lambda x: x["score"], reverse=True)
43
+ top_results = scored[:5]
44
+ retrieval_scores = [s["score"] for s in top_results]
45
+
46
+ # 3. Build skills context for the LLM
47
+ skills_context = ""
48
+ for rank, s in enumerate(top_results):
49
+ sk = s["skill"]
50
+ skills_context += f"\n--- Skill #{rank+1} (retrieval_score: {s['score']}) ---\n"
51
+ skills_context += f"Category: {sk.get('category', 'Unknown')}\n"
52
+ skills_context += f"Rule: {sk.get('rule', '')}\n"
53
+ skills_context += f"Rationale: {sk.get('rationale', '')}\n"
54
+ skills_context += f"Evidence: {json.dumps(sk.get('evidence', []))}\n"
55
+ skills_context += f"Compiled Confidence: {sk.get('confidence', 'unknown')}\n"
56
+
57
+ # 4. Prompt the LLM - no example confidence values to bias it
58
+ prompt = """You are the Kernl Brain Agent. You have access to this company's compiled operational skills (retrieved below, ranked by relevance).
59
+
60
+ Your task:
61
+ 1. Read the scenario and optional JSON context carefully.
62
+ 2. Examine the retrieved skills and their retrieval_scores.
63
+ 3. Determine whether any skill clearly applies to this scenario.
64
+ 4. If a skill applies, state the specific recommended action from that skill's rule.
65
+ 5. If NO skill applies, or if the input is nonsensical/gibberish, say so honestly.
66
+
67
+ CONFIDENCE SCORING - base it on real signals:
68
+ - retrieval_score < 0.3 -> scenario is likely unrelated to any skill -> confidence < 0.2
69
+ - retrieval_score 0.3-0.5 -> weak match -> confidence 0.2-0.5
70
+ - retrieval_score 0.5-0.7 -> moderate match -> confidence 0.5-0.75
71
+ - retrieval_score > 0.7 AND rule clearly addresses the scenario -> confidence 0.75-0.95
72
+ - Never exceed 0.95 unless the match is exact and unambiguous.
73
+ - Gibberish or nonsensical input -> confidence 0.0, recommended_action = "unable to determine"
74
+
75
+ Respond with ONLY a JSON object (no markdown fences, no text outside the JSON):
76
+ {
77
+ "recommended_action": "the specific action to take",
78
+ "rule_applied": "exact rule text from the best matching skill",
79
+ "evidence": ["evidence items from the skill"],
80
+ "skill_matched": "the category of the matched skill",
81
+ "confidence": 0.0,
82
+ "reasoning": "explain why this skill applies and how you chose the confidence level"
83
+ }"""
84
+
85
+ user_content = f"--- Scenario ---\n{scenario}\n\n--- Additional Context ---\n{json.dumps(context or {})}\n\n--- Retrieved Skills (ranked by relevance) ---\n{skills_context}"
86
+
87
+ response_str = await llm_call(prompt, user_content)
88
+ result = _parse_json(response_str)
89
+ result["retrieval_scores"] = retrieval_scores
90
+ return result
91
+
92
+
93
+ async def _baseline_query(scenario: str, context: dict = None) -> dict:
94
+ """Without-brain baseline: LLM answers with zero company context."""
95
+ prompt = """You are a generic AI assistant. You have NO company-specific knowledge or policies.
96
+ Answer based only on general industry standards. Be honest about your lack of specific context.
97
+ Respond with ONLY a JSON object:
98
+ {
99
+ "recommended_action": "your general recommendation",
100
+ "rule_applied": "general industry standard you referenced",
101
+ "evidence": [],
102
+ "skill_matched": "none",
103
+ "confidence": 0.3,
104
+ "retrieval_scores": [],
105
+ "reasoning": "explain your reasoning, noting you lack company-specific context"
106
+ }"""
107
+ user_content = f"Scenario: {scenario}\nContext: {json.dumps(context or {})}"
108
+ response_str = await llm_call(prompt, user_content)
109
+ return _parse_json(response_str)
110
+
111
+
112
+ def _parse_json(raw: str) -> dict:
113
+ """Parse LLM response as JSON, stripping markdown fences."""
114
+ try:
115
+ clean = raw.strip()
116
+ if clean.startswith("```json"):
117
+ clean = clean[7:]
118
+ if clean.startswith("```"):
119
+ clean = clean[3:]
120
+ if clean.endswith("```"):
121
+ clean = clean[:-3]
122
+ return json.loads(clean.strip())
123
+ except Exception as e:
124
+ return {
125
+ "recommended_action": "Failed to parse LLM response",
126
+ "rule_applied": "none",
127
+ "evidence": [],
128
+ "skill_matched": "none",
129
+ "confidence": 0.0,
130
+ "retrieval_scores": [],
131
+ "reasoning": f"JSON parse error: {e}. Raw: {raw[:500]}"
132
+ }
133
+
134
+
135
+ def _error_response(msg: str) -> dict:
136
+ return {
137
+ "recommended_action": msg,
138
+ "rule_applied": "none",
139
+ "evidence": [],
140
+ "skill_matched": "none",
141
+ "confidence": 0.0,
142
+ "retrieval_scores": [],
143
+ "reasoning": msg
144
+ }
backend/db/schema.sql ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Run these in Supabase SQL editor before starting
2
+
3
+ CREATE TABLE companies (
4
+ id TEXT PRIMARY KEY,
5
+ name TEXT NOT NULL,
6
+ created_at TIMESTAMPTZ DEFAULT now()
7
+ );
8
+
9
+ INSERT INTO companies VALUES ('rivanly-inc', 'Rivanly Inc.', now());
10
+
11
+ CREATE TABLE skills_files (
12
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
13
+ company_id TEXT REFERENCES companies(id),
14
+ version TEXT NOT NULL,
15
+ brain_json JSONB NOT NULL,
16
+ source_hashes JSONB NOT NULL,
17
+ compiled_at TIMESTAMPTZ DEFAULT now(),
18
+ is_current BOOLEAN DEFAULT false
19
+ );
20
+
21
+ CREATE UNIQUE INDEX idx_skills_files_current ON skills_files(company_id) WHERE is_current = true;
22
+
23
+ CREATE TABLE skills (
24
+ id TEXT NOT NULL,
25
+ company_id TEXT REFERENCES companies(id),
26
+ skills_file_id UUID REFERENCES skills_files(id),
27
+ name TEXT NOT NULL,
28
+ domain TEXT NOT NULL,
29
+ version TEXT NOT NULL,
30
+ confidence FLOAT NOT NULL,
31
+ stale BOOLEAN DEFAULT false,
32
+ review_required BOOLEAN DEFAULT false,
33
+ skill_json JSONB NOT NULL,
34
+ PRIMARY KEY (id, company_id, skills_file_id)
35
+ );
36
+
37
+ CREATE TABLE source_files (
38
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
39
+ company_id TEXT REFERENCES companies(id),
40
+ filename TEXT NOT NULL,
41
+ sha256 TEXT NOT NULL,
42
+ storage_path TEXT NOT NULL,
43
+ uploaded_at TIMESTAMPTZ DEFAULT now()
44
+ );
45
+
46
+ CREATE TABLE compile_runs (
47
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
48
+ company_id TEXT REFERENCES companies(id),
49
+ status TEXT NOT NULL CHECK (status IN ('started','running','complete','error')),
50
+ started_at TIMESTAMPTZ DEFAULT now(),
51
+ completed_at TIMESTAMPTZ,
52
+ duration_ms INTEGER,
53
+ result_version TEXT,
54
+ error_detail TEXT
55
+ );
56
+
57
+ CREATE INDEX idx_skills_files_company ON skills_files(company_id, compiled_at DESC);
58
+ CREATE INDEX idx_skills_company ON skills(company_id);
backend/db/supabase.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from supabase import create_client, Client
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
8
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
9
+
10
+ if SUPABASE_URL and SUPABASE_KEY:
11
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
12
+ else:
13
+ # We allow the app to start without Supabase for local testing if needed,
14
+ # but actual DB calls will fail if not provided.
15
+ supabase = None
16
+
17
+ def get_client():
18
+ return supabase
19
+
20
+ def get_current_brain(company_id: str):
21
+ if not supabase: return None
22
+ res = supabase.table("skills_files").select("*").eq("company_id", company_id).eq("is_current", True).execute()
23
+ if res.data:
24
+ return res.data[0]
25
+ return None
26
+
27
+ def save_skills_file(data: dict):
28
+ if not supabase: return None
29
+ res = supabase.table("skills_files").insert(data).execute()
30
+ return res.data
31
+
32
+ def save_compile_run(data: dict):
33
+ if not supabase: return None
34
+ res = supabase.table("compile_runs").insert(data).execute()
35
+ return res.data
36
+
37
+ def update_compile_run(run_id: str, data: dict):
38
+ if not supabase: return None
39
+ res = supabase.table("compile_runs").update(data).eq("id", run_id).execute()
40
+ return res.data
41
+
42
+ def get_source_hashes(company_id: str):
43
+ if not supabase: return {}
44
+ # Get the latest current brain
45
+ brain = get_current_brain(company_id)
46
+ if brain:
47
+ return brain.get("source_hashes", {})
48
+ return {}
49
+
50
+ def save_source_file(data: dict):
51
+ if not supabase: return None
52
+ res = supabase.table("source_files").insert(data).execute()
53
+ return res.data
54
+
55
+ def get_skills_by_brain_id(brain_id: str):
56
+ if not supabase: return []
57
+ res = supabase.table("skills").select("*").eq("skills_file_id", brain_id).execute()
58
+ return res.data
59
+
60
+ def insert_skills(data: list):
61
+ if not supabase: return None
62
+ res = supabase.table("skills").insert(data).execute()
63
+ return res.data
backend/graph/graph.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END
2
+ from backend.graph.state import BrainState
3
+ from backend.graph.nodes.load_and_chunk import load_and_chunk
4
+ from backend.graph.nodes.cluster_evidence import cluster_evidence
5
+ from backend.graph.nodes.synthesize_skills import synthesize_skills
6
+ from backend.graph.nodes.quality_normalize import quality_normalize
7
+ from backend.graph.nodes.write_brain import write_brain
8
+
9
+
10
+ def build_compilation_graph() -> StateGraph:
11
+ """
12
+ Linear 5-node pipeline:
13
+ load_and_chunk → cluster_evidence → synthesize_skills → quality_normalize → write_brain
14
+ """
15
+ workflow = StateGraph(BrainState)
16
+
17
+ workflow.add_node("load_and_chunk", load_and_chunk)
18
+ workflow.add_node("cluster_evidence", cluster_evidence)
19
+ workflow.add_node("synthesize_skills", synthesize_skills)
20
+ workflow.add_node("quality_normalize", quality_normalize)
21
+ workflow.add_node("write_brain", write_brain)
22
+
23
+ workflow.set_entry_point("load_and_chunk")
24
+ workflow.add_edge("load_and_chunk", "cluster_evidence")
25
+ workflow.add_edge("cluster_evidence", "synthesize_skills")
26
+ workflow.add_edge("synthesize_skills", "quality_normalize")
27
+ workflow.add_edge("quality_normalize", "write_brain")
28
+ workflow.add_edge("write_brain", END)
29
+
30
+ return workflow.compile()
backend/graph/nodes/cluster_evidence.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Node 2: Embed all chunks and cluster them by domain using the LLM.
3
+ Emits SSE stage: EMBEDDING
4
+ """
5
+ import json
6
+ from backend.graph.state import BrainState
7
+ from backend.llm import llm_call, get_embeddings
8
+ from backend.sse import emit
9
+
10
+
11
+ async def cluster_evidence(state: BrainState) -> dict:
12
+ job_id = state["job_id"]
13
+ chunks = state.get("chunks", [])
14
+
15
+ print(f"[{job_id}] Node cluster_evidence started with {len(chunks)} chunks")
16
+
17
+ if not chunks:
18
+ await emit(job_id, "stage", {"name": "EMBEDDING", "detail": "No chunks to embed"})
19
+ return {"clusters": {"domains": {}}}
20
+
21
+ await emit(job_id, "stage", {"name": "EMBEDDING", "detail": f"Embedding {len(chunks)} chunks"})
22
+
23
+ # Build a numbered summary of each chunk for the LLM
24
+ summaries = []
25
+ for i, c in enumerate(chunks):
26
+ # Truncate long chunks for the categorization prompt
27
+ preview = c["text"][:300].replace("\n", " ")
28
+ summaries.append(f"[{i}] ({c['source_file']}) {preview}")
29
+
30
+ chunk_list_text = "\n".join(summaries)
31
+
32
+ prompt = """You are an operations analyst. Below is a numbered list of text chunks extracted from a company's internal documents (SOPs, Slack messages, support tickets).
33
+
34
+ Categorize each chunk into an operational domain. Use clear domain names like:
35
+ "Customer Support", "Engineering", "Sales", "Human Resources", "Finance", "Operations", etc.
36
+
37
+ Return ONLY a valid JSON object mapping domain names to arrays of chunk indices.
38
+ Example: {"Customer Support": [0, 3, 5], "Engineering": [1, 2], "Sales": [4]}
39
+
40
+ Every chunk index must appear exactly once. Do not skip any."""
41
+
42
+ response_str = await llm_call(prompt, chunk_list_text)
43
+
44
+ try:
45
+ clean = response_str.strip()
46
+ if clean.startswith("```json"):
47
+ clean = clean[7:]
48
+ if clean.startswith("```"):
49
+ clean = clean[3:]
50
+ if clean.endswith("```"):
51
+ clean = clean[:-3]
52
+ domains = json.loads(clean.strip())
53
+ except Exception as e:
54
+ print(f"[cluster_evidence] Failed to parse LLM clustering: {e}")
55
+ # Fallback: put all chunks in one cluster
56
+ domains = {"General": list(range(len(chunks)))}
57
+
58
+ await emit(job_id, "stage", {
59
+ "name": "EMBEDDING_DONE",
60
+ "detail": f"Clustered into {len(domains)} domains: {list(domains.keys())}",
61
+ })
62
+
63
+ print(f"[{job_id}] Node cluster_evidence finished with {len(domains)} domains")
64
+ return {"clusters": {"domains": domains}}
backend/graph/nodes/load_and_chunk.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Node 1: Load source files from disk and chunk them.
3
+ Emits SSE stages: LOADING_DOCS, CHUNKING
4
+ """
5
+ import os
6
+ import json
7
+ import hashlib
8
+ import time
9
+ from backend.graph.state import BrainState
10
+ from backend.sse import emit
11
+
12
+
13
+ async def load_and_chunk(state: BrainState) -> dict:
14
+ company_id = state["company_id"]
15
+ job_id = state["job_id"]
16
+
17
+ print(f"[{job_id}] Node load_and_chunk started")
18
+ await emit(job_id, "stage", {"name": "LOADING_DOCS", "detail": f"Reading sources for {company_id}"})
19
+
20
+ # Read files from the company-specific directory
21
+ # __file__ is backend/graph/nodes/load_and_chunk.py
22
+ base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
23
+ sources_dir = os.path.join(base, "data", "sources", company_id)
24
+
25
+ if not os.path.isdir(sources_dir):
26
+ await emit(job_id, "pipeline_error", {"error": f"No source directory found: data/sources/{company_id}/"})
27
+ print(f"[{job_id}] Node load_and_chunk failed (Missing dir: {sources_dir})")
28
+ return {"errors": [f"Missing directory: {sources_dir}"], "source_files": [], "chunks": []}
29
+
30
+ source_files = []
31
+ for filename in sorted(os.listdir(sources_dir)):
32
+ filepath = os.path.join(sources_dir, filename)
33
+ if not os.path.isfile(filepath):
34
+ continue
35
+ with open(filepath, "r", encoding="utf-8") as f:
36
+ content = f.read()
37
+ doc_type = _detect_type(filename)
38
+ source_files.append({
39
+ "filename": filename,
40
+ "content": content,
41
+ "sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
42
+ "doc_type": doc_type,
43
+ })
44
+
45
+ await emit(job_id, "stage", {
46
+ "name": "CHUNKING",
47
+ "detail": f"Splitting {len(source_files)} files into chunks",
48
+ })
49
+
50
+ chunks = []
51
+ for sf in source_files:
52
+ if sf["doc_type"] == "notion_md":
53
+ chunks.extend(_chunk_markdown(sf))
54
+ elif sf["doc_type"] == "slack_json":
55
+ chunks.extend(_chunk_slack(sf))
56
+ elif sf["doc_type"] == "tickets_json":
57
+ chunks.extend(_chunk_tickets(sf))
58
+ else:
59
+ # Treat unknown as plain text
60
+ chunks.append({
61
+ "text": sf["content"],
62
+ "source_file": sf["filename"],
63
+ "chunk_index": 0,
64
+ "doc_type": sf["doc_type"],
65
+ })
66
+
67
+ await emit(job_id, "stage", {
68
+ "name": "CHUNKING_DONE",
69
+ "detail": f"Produced {len(chunks)} chunks from {len(source_files)} files",
70
+ })
71
+
72
+ print(f"[{job_id}] Node load_and_chunk finished (chunks: {len(chunks)})")
73
+ return {"source_files": source_files, "chunks": chunks}
74
+
75
+
76
+ # --- Helpers ---
77
+
78
+ def _detect_type(filename: str) -> str:
79
+ fn = filename.lower()
80
+ if fn.endswith(".json"):
81
+ if "slack" in fn:
82
+ return "slack_json"
83
+ if "ticket" in fn or "zendesk" in fn:
84
+ return "tickets_json"
85
+ return "json"
86
+ if fn.endswith(".md"):
87
+ return "notion_md"
88
+ return "unknown"
89
+
90
+
91
+ def _chunk_markdown(sf: dict) -> list:
92
+ """Split a markdown file by ## headers. Each section is a chunk."""
93
+ content = sf["content"]
94
+ sections = []
95
+ current_header = "Introduction"
96
+ current_body = []
97
+
98
+ for line in content.split("\n"):
99
+ if line.startswith("## "):
100
+ if current_body:
101
+ sections.append((current_header, "\n".join(current_body).strip()))
102
+ current_header = line.lstrip("# ").strip()
103
+ current_body = []
104
+ else:
105
+ current_body.append(line)
106
+
107
+ if current_body:
108
+ sections.append((current_header, "\n".join(current_body).strip()))
109
+
110
+ chunks = []
111
+ for i, (header, body) in enumerate(sections):
112
+ if not body:
113
+ continue
114
+ chunks.append({
115
+ "text": f"[{header}] {body}",
116
+ "source_file": sf["filename"],
117
+ "chunk_index": i,
118
+ "doc_type": "notion_md",
119
+ "section_header": header,
120
+ })
121
+ return chunks
122
+
123
+
124
+ def _chunk_slack(sf: dict) -> list:
125
+ """Each Slack message is one chunk."""
126
+ try:
127
+ messages = json.loads(sf["content"])
128
+ except json.JSONDecodeError:
129
+ return []
130
+ chunks = []
131
+ for i, msg in enumerate(messages):
132
+ text = msg.get("text", "")
133
+ if not text:
134
+ continue
135
+ user = msg.get("user", "unknown")
136
+ channel = msg.get("channel", "unknown")
137
+ chunks.append({
138
+ "text": f"[Slack #{channel} @{user}] {text}",
139
+ "source_file": sf["filename"],
140
+ "chunk_index": i,
141
+ "doc_type": "slack_json",
142
+ })
143
+ return chunks
144
+
145
+
146
+ def _chunk_tickets(sf: dict) -> list:
147
+ """Each ticket is one chunk."""
148
+ try:
149
+ tickets = json.loads(sf["content"])
150
+ except json.JSONDecodeError:
151
+ return []
152
+ chunks = []
153
+ for i, tkt in enumerate(tickets):
154
+ parts = []
155
+ if tkt.get("subject"):
156
+ parts.append(f"Subject: {tkt['subject']}")
157
+ if tkt.get("description"):
158
+ parts.append(f"Description: {tkt['description']}")
159
+ if tkt.get("resolution"):
160
+ parts.append(f"Resolution: {tkt['resolution']}")
161
+ if tkt.get("priority"):
162
+ parts.append(f"Priority: {tkt['priority']}")
163
+ if tkt.get("customer_plan"):
164
+ parts.append(f"Plan: {tkt['customer_plan']}")
165
+ text = " | ".join(parts)
166
+ if not text:
167
+ continue
168
+ chunks.append({
169
+ "text": f"[Zendesk Ticket] {text}",
170
+ "source_file": sf["filename"],
171
+ "chunk_index": i,
172
+ "doc_type": "tickets_json",
173
+ })
174
+ return chunks
backend/graph/nodes/quality_normalize.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Node 4: De-duplicate skills, resolve conflicts, score confidence, enforce schema.
3
+ Emits SSE stage: QUALITY_CHECK
4
+ """
5
+ import json
6
+ from backend.graph.state import BrainState
7
+ from backend.llm import llm_call
8
+ from backend.sse import emit
9
+
10
+
11
+ async def quality_normalize(state: BrainState) -> dict:
12
+ job_id = state["job_id"]
13
+ raw_skills = state.get("raw_skills", [])
14
+
15
+ print(f"[{job_id}] Node quality_normalize started with {len(raw_skills)} raw skills")
16
+
17
+ if not raw_skills:
18
+ await emit(job_id, "stage", {"name": "QUALITY_CHECK", "detail": "No skills to normalize"})
19
+ print(f"[{job_id}] Node quality_normalize finished (0 skills)")
20
+ return {"skills_file": {"skills": []}}
21
+
22
+ await emit(job_id, "stage", {
23
+ "name": "QUALITY_CHECK",
24
+ "detail": f"Normalizing {len(raw_skills)} raw skills",
25
+ })
26
+
27
+ prompt = """You are a quality assurance agent for an operational skills file.
28
+
29
+ Below is a raw list of skills extracted from company documents. Your job:
30
+
31
+ 1. DEDUPLICATE: merge skills that describe the same rule (keep the most complete version).
32
+ 2. RESOLVE CONFLICTS: if two skills contradict, keep both but note the conflict in the rationale. Prefer observed behavior (from Slack/tickets) over stated policy (from SOPs) when they conflict.
33
+ 3. SCORE CONFIDENCE (0.0 to 1.0) for each skill based on:
34
+ - 0.9–1.0: multiple confirming sources, clear unambiguous rule
35
+ - 0.7–0.89: single strong source or multiple weak sources
36
+ - 0.5–0.69: only one source, or some ambiguity
37
+ - 0.3–0.49: weak evidence or significant ambiguity
38
+ - < 0.3: speculative or poorly supported
39
+ 4. ENFORCE SCHEMA: every skill must have: id, category, rule, rationale, evidence (array), confidence (float).
40
+
41
+ Return ONLY a JSON object:
42
+ {
43
+ "skills": [
44
+ {
45
+ "id": "skill_slug",
46
+ "category": "Domain Name",
47
+ "rule": "The specific rule text",
48
+ "rationale": "Why this rule exists",
49
+ "evidence": ["source reference 1", "source reference 2"],
50
+ "confidence": 0.85
51
+ }
52
+ ]
53
+ }"""
54
+
55
+ skills_text = json.dumps(raw_skills, indent=2)
56
+ print(f"[{job_id}] Requesting quality normalization...")
57
+ response_str = await llm_call(prompt, skills_text, max_tokens=8192)
58
+ print(f"[{job_id}] Received quality normalization response")
59
+
60
+ try:
61
+ clean = response_str.strip()
62
+ if clean.startswith("```json"):
63
+ clean = clean[7:]
64
+ if clean.startswith("```"):
65
+ clean = clean[3:]
66
+ if clean.endswith("```"):
67
+ clean = clean[:-3]
68
+ data = json.loads(clean.strip())
69
+ final_skills = data.get("skills", raw_skills)
70
+ except Exception as e:
71
+ print(f"[{job_id}] [quality_normalize] Parse error: {e}")
72
+ # Fallback: use raw skills with default confidence
73
+ final_skills = raw_skills
74
+ for sk in final_skills:
75
+ sk.setdefault("confidence", 0.5)
76
+
77
+ await emit(job_id, "stage", {
78
+ "name": "QUALITY_CHECK_DONE",
79
+ "detail": f"Final skills count: {len(final_skills)} (from {len(raw_skills)} raw)",
80
+ })
81
+
82
+ print(f"[{job_id}] Node quality_normalize finished (final skills: {len(final_skills)})")
83
+ return {"skills_file": {"skills": final_skills}}
backend/graph/nodes/synthesize_skills.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Node 3: For each domain cluster, call vLLM to synthesize structured skills.
3
+ Emits SSE stage: SYNTHESIZING_SKILLS
4
+ """
5
+ import json
6
+ import uuid
7
+ from backend.graph.state import BrainState
8
+ from backend.llm import llm_call
9
+ from backend.sse import emit
10
+
11
+
12
+ async def synthesize_skills(state: BrainState) -> dict:
13
+ job_id = state["job_id"]
14
+ chunks = state.get("chunks", [])
15
+ clusters = state.get("clusters", {})
16
+ domains = clusters.get("domains", {})
17
+
18
+ print(f"[{job_id}] Node synthesize_skills started with {len(domains)} domains")
19
+
20
+ if not domains:
21
+ await emit(job_id, "stage", {"name": "SYNTHESIZING_SKILLS", "detail": "No clusters to synthesize"})
22
+ print(f"[{job_id}] Node synthesize_skills finished (0 domains)")
23
+ return {"raw_skills": []}
24
+
25
+ await emit(job_id, "stage", {
26
+ "name": "SYNTHESIZING_SKILLS",
27
+ "detail": f"Synthesizing skills for {len(domains)} domains",
28
+ })
29
+
30
+ all_skills = []
31
+
32
+ for domain_name, chunk_indices in domains.items():
33
+ # Gather the actual chunk texts for this domain
34
+ domain_chunks = []
35
+ for idx in chunk_indices:
36
+ if 0 <= idx < len(chunks):
37
+ domain_chunks.append(chunks[idx])
38
+
39
+ if not domain_chunks:
40
+ continue
41
+
42
+ chunk_text = "\n\n".join([c["text"] for c in domain_chunks])
43
+ source_files = list(set(c["source_file"] for c in domain_chunks))
44
+
45
+ prompt = f"""You are a Principal Operations Architect analyzing the "{domain_name}" domain.
46
+
47
+ Below are real excerpts from a company's internal documents (SOPs, Slack messages, support tickets) related to {domain_name}.
48
+
49
+ Your job: extract every distinct operational rule, policy, process, or decision pattern you can find.
50
+
51
+ For EACH skill, provide:
52
+ - id: a unique identifier (use a short slug like "refund_loyal_customer")
53
+ - category: "{domain_name}"
54
+ - rule: the specific, actionable rule or process (be precise — include thresholds, timeframes, approvals)
55
+ - rationale: why this rule exists (based on the evidence)
56
+ - evidence: array of specific quotes or references from the source chunks that support this rule
57
+ - source_files: which files this came from
58
+
59
+ Rules for quality:
60
+ - Extract what the documents ACTUALLY say, not what you assume.
61
+ - If there are contradictions (e.g., SOP says X but Slack shows Y), note BOTH and state which takes precedence in practice.
62
+ - Do NOT invent rules that aren't supported by the text below.
63
+ - Each rule should be specific enough that a human could follow it without additional context.
64
+
65
+ Respond with ONLY a JSON object:
66
+ {{
67
+ "skills": [
68
+ {{
69
+ "id": "refund_loyal_customer",
70
+ "category": "{domain_name}",
71
+ "rule": "Approve refunds up to 45 days for customers with >2 years tenure",
72
+ "rationale": "Exception applied over standard 30-day limit for loyal customers",
73
+ "evidence": ["slack_export_support.json: Mike approved 45-day refund for Acme Corp"],
74
+ "source_files": ["slack_export_support.json", "notion_refund_sop.md"]
75
+ }}
76
+ ]
77
+ }}"""
78
+
79
+ print(f"[{job_id}] Requesting skills for domain '{domain_name}'...")
80
+ response_str = await llm_call(prompt, chunk_text)
81
+ print(f"[{job_id}] Received skills response for domain '{domain_name}'")
82
+
83
+ try:
84
+ clean = response_str.strip()
85
+ if clean.startswith("```json"):
86
+ clean = clean[7:]
87
+ if clean.startswith("```"):
88
+ clean = clean[3:]
89
+ if clean.endswith("```"):
90
+ clean = clean[:-3]
91
+ data = json.loads(clean.strip())
92
+ domain_skills = data.get("skills", [])
93
+ except Exception as e:
94
+ print(f"[{job_id}] [synthesize_skills] Parse error for {domain_name}: {e}")
95
+ domain_skills = []
96
+
97
+ # Ensure every skill has an id
98
+ for sk in domain_skills:
99
+ if not sk.get("id"):
100
+ sk["id"] = str(uuid.uuid4())[:8]
101
+ sk["category"] = domain_name # ensure consistency
102
+
103
+ all_skills.extend(domain_skills)
104
+
105
+ await emit(job_id, "stage", {
106
+ "name": "SYNTHESIZING_SKILLS",
107
+ "detail": f"{domain_name}: extracted {len(domain_skills)} skills",
108
+ })
109
+
110
+ print(f"[{job_id}] Node synthesize_skills finished (extracted {len(all_skills)} skills overall)")
111
+ return {"raw_skills": all_skills}
backend/graph/nodes/write_brain.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Node 5: Write the final skills file to the database.
3
+ Emits SSE stage: WRITING_DB, then pipeline_complete.
4
+ """
5
+ import time
6
+ import json
7
+ import uuid
8
+ import datetime
9
+ from backend.graph.state import BrainState
10
+ from backend.db.supabase import get_client
11
+ from backend.sse import emit
12
+
13
+
14
+ async def write_brain(state: BrainState) -> dict:
15
+ job_id = state.get("job_id")
16
+ company_id = state.get("company_id")
17
+ skills_file = state.get("skills_file", {})
18
+ skills = skills_file.get("skills", [])
19
+ start_time = state.get("start_time", time.time())
20
+ duration_ms = int((time.time() - start_time) * 1000)
21
+
22
+ print(f"[{job_id}] Node write_brain started for {company_id}")
23
+
24
+ await emit(job_id, "stage", {"name": "WRITING_DB", "detail": f"Persisting {len(skills)} skills"})
25
+
26
+ db = get_client()
27
+ if not db:
28
+ await emit(job_id, "pipeline_error", {"error": "Database connection failed"})
29
+ print(f"[{job_id}] Node write_brain failed (no DB client)")
30
+ return {"errors": ["DB connection failed in write_brain"]}
31
+
32
+ try:
33
+ now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat()
34
+ version_str = f"v_{int(time.time())}"
35
+
36
+ source_hashes = {}
37
+ for f in state.get("source_files", []):
38
+ if "filename" in f and "sha256" in f:
39
+ source_hashes[f["filename"]] = f["sha256"]
40
+
41
+ # Mark previous brain as not current
42
+ db.table("skills_files").update(
43
+ {"is_current": False}
44
+ ).eq("company_id", company_id).eq("is_current", True).execute()
45
+
46
+ # Insert new brain
47
+ sf_res = db.table("skills_files").insert({
48
+ "company_id": company_id,
49
+ "version": version_str,
50
+ "brain_json": skills_file,
51
+ "source_hashes": source_hashes,
52
+ "is_current": True,
53
+ }).execute()
54
+
55
+ sf_id = sf_res.data[0]["id"]
56
+
57
+ # Insert individual skills
58
+ for skill in skills:
59
+ db.table("skills").insert({
60
+ "id": skill.get("id", str(uuid.uuid4())[:8]),
61
+ "company_id": company_id,
62
+ "skills_file_id": sf_id,
63
+ "name": skill.get("rule", "Unknown")[:200],
64
+ "domain": skill.get("category", "general"),
65
+ "version": version_str,
66
+ "confidence": float(skill.get("confidence", 0.5)),
67
+ "skill_json": skill,
68
+ }).execute()
69
+
70
+ # Update compile run
71
+ db.table("compile_runs").update({
72
+ "status": "complete",
73
+ "completed_at": now_iso,
74
+ "duration_ms": duration_ms,
75
+ "result_version": version_str,
76
+ }).eq("id", job_id).execute()
77
+
78
+ except Exception as e:
79
+ print(f"[{job_id}] [write_brain] DB Error: {e}")
80
+ await emit(job_id, "pipeline_error", {"error": str(e)})
81
+ return {"errors": [f"write_brain DB error: {e}"]}
82
+
83
+ await emit(job_id, "stage", {
84
+ "name": "DONE",
85
+ "detail": f"Brain {version_str} written: {len(skills)} skills, {len(source_hashes)} sources, {duration_ms}ms",
86
+ })
87
+ await emit(job_id, "pipeline_complete", {
88
+ "status": "success",
89
+ "version": version_str,
90
+ "skills_count": len(skills),
91
+ "source_count": len(source_hashes),
92
+ "duration_ms": duration_ms,
93
+ })
94
+
95
+ print(f"[{job_id}] Node write_brain finished successfully (version: {version_str})")
96
+ return {}
backend/graph/state.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, Annotated, List, Dict, Any
2
+ import operator
3
+
4
+ class BrainState(TypedDict):
5
+ company_id: str
6
+ job_id: str
7
+ source_files: List[Dict[str, Any]] # [{filename, content, sha256, doc_type}]
8
+ chunks: List[Dict[str, Any]] # [{text, source_file, chunk_index, doc_type}]
9
+ clusters: Dict[str, Any] # {domains: {domain_name: [chunk_indices]}}
10
+ raw_skills: List[Dict[str, Any]] # skills before quality pass
11
+ skills_file: Dict[str, Any] # final {skills: [...]}
12
+ brain_version: str
13
+ start_time: float
14
+ errors: Annotated[List[str], operator.add]
backend/llm.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import numpy as np
4
+ from openai import AsyncOpenAI
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ VLLM_BASE_URL = os.getenv("VLLM_BASE_URL", "http://localhost:8000/v1")
10
+ MODEL_NAME = "RedHatAI/Qwen2.5-72B-Instruct-FP8-dynamic"
11
+
12
+ llm = AsyncOpenAI(base_url=VLLM_BASE_URL, api_key="not-needed", timeout=120.0)
13
+
14
+ # --- Embedding model (local, fast, centralized here) ---
15
+ _embedding_model = None
16
+
17
+ def _get_embedding_model():
18
+ global _embedding_model
19
+ if _embedding_model is None:
20
+ from sentence_transformers import SentenceTransformer
21
+ _embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
22
+ return _embedding_model
23
+
24
+ def get_embedding(text: str) -> list:
25
+ """Return a single embedding vector as a Python list."""
26
+ model = _get_embedding_model()
27
+ return model.encode(text).tolist()
28
+
29
+ def get_embeddings(texts: list) -> list:
30
+ """Return a list of embedding vectors."""
31
+ model = _get_embedding_model()
32
+ return [v.tolist() for v in model.encode(texts)]
33
+
34
+ def cosine_similarity(v1, v2) -> float:
35
+ """Cosine similarity between two vectors."""
36
+ a, b = np.array(v1), np.array(v2)
37
+ denom = np.linalg.norm(a) * np.linalg.norm(b)
38
+ if denom == 0:
39
+ return 0.0
40
+ return float(np.dot(a, b) / denom)
41
+
42
+ async def check_vllm_health() -> dict:
43
+ """Ping the vLLM /v1/models endpoint. Returns status dict."""
44
+ try:
45
+ response = await llm.models.list()
46
+ models = [m.id for m in response.data]
47
+ return {"healthy": True, "models": models, "url": VLLM_BASE_URL}
48
+ except Exception as e:
49
+ return {"healthy": False, "error": str(e), "url": VLLM_BASE_URL}
50
+
51
+ async def llm_call(system_prompt: str, user_content: str, temperature: float = 0.1, max_tokens: int = 4096) -> str:
52
+ """Single centralized LLM call through vLLM. Raises on failure."""
53
+ try:
54
+ response = await llm.chat.completions.create(
55
+ model=MODEL_NAME,
56
+ messages=[
57
+ {"role": "system", "content": system_prompt},
58
+ {"role": "user", "content": user_content}
59
+ ],
60
+ temperature=temperature,
61
+ max_tokens=max_tokens
62
+ )
63
+ return response.choices[0].message.content
64
+ except Exception as e:
65
+ raise RuntimeError(f"vLLM call failed ({VLLM_BASE_URL}): {e}")
backend/main.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File, Form
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import StreamingResponse
4
+ import os
5
+ import uuid
6
+ import time
7
+ import json
8
+ import hashlib
9
+ import shutil
10
+
11
+ from backend.graph.graph import build_compilation_graph
12
+ from backend.sse import event_bus, emit
13
+ from backend.agent.brain_agent import handle_agent_query
14
+ from backend.db.supabase import get_client
15
+ from backend.llm import check_vllm_health
16
+ from backend.models.schemas import CompileRequest, AgentHandleRequest, AgentQueryRequest
17
+
18
+ app = FastAPI(title="Kernl API", version="2.0.0")
19
+
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"],
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
29
+ SOURCES_ROOT = os.path.join(BASE_DIR, "data", "sources")
30
+
31
+
32
+ # ─────────────────────────────────────────────
33
+ # Health
34
+ # ─────────────────────────────────────────────
35
+ @app.get("/health")
36
+ async def health_check():
37
+ vllm = await check_vllm_health()
38
+ db = get_client()
39
+ return {
40
+ "status": "ok",
41
+ "vllm": vllm,
42
+ "database": "connected" if db else "not configured",
43
+ }
44
+
45
+
46
+ # ─────────────────────────────────────────────
47
+ # Source file management
48
+ # ─────────────────────────────────────────────
49
+ def _company_sources_dir(company_id: str) -> str:
50
+ return os.path.join(SOURCES_ROOT, company_id)
51
+
52
+
53
+ @app.post("/sources/upload")
54
+ async def upload_source(company_id: str = Form(...), file: UploadFile = File(...)):
55
+ """Upload a source file for a company."""
56
+ dest_dir = _company_sources_dir(company_id)
57
+ os.makedirs(dest_dir, exist_ok=True)
58
+
59
+ content = await file.read()
60
+ filepath = os.path.join(dest_dir, file.filename)
61
+ with open(filepath, "wb") as f:
62
+ f.write(content)
63
+
64
+ file_hash = hashlib.sha256(content).hexdigest()
65
+
66
+ # Record in DB
67
+ db = get_client()
68
+ if db:
69
+ try:
70
+ db.table("source_files").insert({
71
+ "company_id": company_id,
72
+ "filename": file.filename,
73
+ "sha256": file_hash,
74
+ "storage_path": f"data/sources/{company_id}/{file.filename}",
75
+ }).execute()
76
+ except Exception as e:
77
+ print(f"[upload] DB record error: {e}")
78
+
79
+ return {"filename": file.filename, "sha256": file_hash, "status": "uploaded"}
80
+
81
+
82
+ @app.get("/sources/{company_id}")
83
+ async def list_sources(company_id: str):
84
+ """List all source files for a company."""
85
+ src_dir = _company_sources_dir(company_id)
86
+ if not os.path.isdir(src_dir):
87
+ return {"files": []}
88
+ files = []
89
+ for fn in sorted(os.listdir(src_dir)):
90
+ fp = os.path.join(src_dir, fn)
91
+ if os.path.isfile(fp):
92
+ with open(fp, "rb") as f:
93
+ content = f.read()
94
+ files.append({
95
+ "filename": fn,
96
+ "size_bytes": len(content),
97
+ "sha256": hashlib.sha256(content).hexdigest(),
98
+ })
99
+ return {"files": files, "company_id": company_id}
100
+
101
+
102
+ @app.delete("/sources/{company_id}/{filename}")
103
+ async def delete_source(company_id: str, filename: str):
104
+ """Delete a source file."""
105
+ filepath = os.path.join(_company_sources_dir(company_id), filename)
106
+ if not os.path.isfile(filepath):
107
+ raise HTTPException(status_code=404, detail=f"File not found: {filename}")
108
+ os.remove(filepath)
109
+
110
+ db = get_client()
111
+ if db:
112
+ try:
113
+ db.table("source_files").delete().eq(
114
+ "company_id", company_id
115
+ ).eq("filename", filename).execute()
116
+ except Exception as e:
117
+ print(f"[delete] DB cleanup error: {e}")
118
+
119
+ return {"status": "deleted", "filename": filename}
120
+
121
+
122
+ # ─────────────────────────────────────────────
123
+ # Compilation pipeline
124
+ # ─────────────────────────────────────────────
125
+ import asyncio
126
+ import traceback
127
+ import datetime
128
+
129
+ async def run_compilation_graph(job_id: str, company_id: str):
130
+ initial_state = {
131
+ "job_id": job_id,
132
+ "company_id": company_id,
133
+ "source_files": [],
134
+ "chunks": [],
135
+ "clusters": {},
136
+ "raw_skills": [],
137
+ "skills_file": {},
138
+ "brain_version": "",
139
+ "start_time": time.time(),
140
+ "errors": [],
141
+ }
142
+
143
+ graph = build_compilation_graph()
144
+
145
+ await emit(job_id, "pipeline_start", {"company_id": company_id})
146
+ try:
147
+ # Prevent indefinite hanging
148
+ await asyncio.wait_for(graph.ainvoke(initial_state), timeout=600.0)
149
+ except Exception as e:
150
+ err_msg = str(e)
151
+ if isinstance(e, asyncio.TimeoutError):
152
+ err_msg = "Pipeline execution timed out after 600 seconds."
153
+
154
+ trace = traceback.format_exc()
155
+ print(f"Graph execution failed for {job_id}:\n{trace}")
156
+
157
+ await emit(job_id, "pipeline_error", {"error": err_msg, "traceback": trace})
158
+ # Update compile run status
159
+ db = get_client()
160
+ if db:
161
+ try:
162
+ db.table("compile_runs").update({
163
+ "status": "error",
164
+ "completed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
165
+ "error_detail": err_msg,
166
+ }).eq("id", job_id).execute()
167
+ except Exception as db_e:
168
+ print(f"Failed to update compile_runs with error status: {db_e}")
169
+
170
+
171
+ @app.post("/compile")
172
+ @app.post("/compile/run")
173
+ async def compile_brain(req: CompileRequest, background_tasks: BackgroundTasks):
174
+ # Verify source directory exists
175
+ src_dir = _company_sources_dir(req.company_id)
176
+ if not os.path.isdir(src_dir) or not os.listdir(src_dir):
177
+ raise HTTPException(
178
+ status_code=400,
179
+ detail=f"No source files found at data/sources/{req.company_id}/. Upload files first.",
180
+ )
181
+
182
+ job_id = str(uuid.uuid4())
183
+ db = get_client()
184
+
185
+ if db:
186
+ try:
187
+ db.table("compile_runs").insert({
188
+ "id": job_id,
189
+ "company_id": req.company_id,
190
+ "status": "running",
191
+ }).execute()
192
+ except Exception as e:
193
+ print(f"Error creating run: {e}")
194
+
195
+ background_tasks.add_task(run_compilation_graph, job_id, req.company_id)
196
+ return {"job_id": job_id, "status": "started"}
197
+
198
+
199
+ @app.get("/compile/{job_id}/stream")
200
+ async def compile_stream(job_id: str):
201
+ return StreamingResponse(
202
+ event_bus.event_generator(job_id),
203
+ media_type="text/event-stream",
204
+ )
205
+
206
+
207
+ @app.get("/compile/{job_id}/status")
208
+ async def compile_status(job_id: str):
209
+ db = get_client()
210
+ if not db:
211
+ return {"status": "unknown", "error_detail": "No DB"}
212
+ res = db.table("compile_runs").select("*").eq("id", job_id).execute()
213
+ if not res.data:
214
+ return {"status": "not_found"}
215
+ return res.data[0]
216
+
217
+
218
+ # ─────────────────────────────────────────────
219
+ # Agent query
220
+ # ─────────────────────────────────────────────
221
+ @app.post("/agent/handle")
222
+ async def agent_handle_endpoint(req: AgentHandleRequest):
223
+ """Legacy endpoint — kept for frontend compat."""
224
+ result = await handle_agent_query(req.company_id, req.scenario, req.context, req.with_brain)
225
+ return result
226
+
227
+
228
+ @app.post("/agent/query")
229
+ async def agent_query_endpoint(req: AgentQueryRequest):
230
+ """New canonical endpoint."""
231
+ result = await handle_agent_query(
232
+ req.company_id,
233
+ req.scenario_text,
234
+ req.json_context,
235
+ req.with_brain,
236
+ )
237
+ return result
238
+
239
+
240
+ # ─────────────────────────────────────────────
241
+ # Skills & brain versions
242
+ # ─────────────────────────────────────────────
243
+ @app.get("/skills")
244
+ async def get_skills_legacy(company_id: str):
245
+ """Legacy endpoint: returns raw brain_json."""
246
+ db = get_client()
247
+ if not db:
248
+ raise HTTPException(status_code=500, detail="Database not connected")
249
+ res = db.table("skills_files").select("brain_json").eq(
250
+ "company_id", company_id
251
+ ).order("compiled_at", desc=True).limit(1).execute()
252
+ if not res.data:
253
+ return {"skills": []}
254
+ return res.data[0]["brain_json"]
255
+
256
+
257
+ @app.get("/skills/{company_id}")
258
+ async def get_skills(company_id: str):
259
+ """Returns detailed skills with metadata."""
260
+ db = get_client()
261
+ if not db:
262
+ raise HTTPException(status_code=500, detail="Database not connected")
263
+
264
+ res = db.table("skills_files").select("*").eq(
265
+ "company_id", company_id
266
+ ).eq("is_current", True).execute()
267
+
268
+ if not res.data:
269
+ return {"skills": [], "version": None, "compiled_at": None}
270
+
271
+ brain = res.data[0]
272
+ skills = brain["brain_json"].get("skills", [])
273
+ return {
274
+ "skills": skills,
275
+ "version": brain["version"],
276
+ "compiled_at": brain["compiled_at"],
277
+ "source_hashes": brain.get("source_hashes", {}),
278
+ "brain_id": brain["id"],
279
+ }
280
+
281
+
282
+ @app.get("/brain/versions/{company_id}")
283
+ async def list_brain_versions(company_id: str):
284
+ """Lists all brain versions for a company."""
285
+ db = get_client()
286
+ if not db:
287
+ raise HTTPException(status_code=500, detail="Database not connected")
288
+
289
+ res = db.table("skills_files").select(
290
+ "id, version, compiled_at, is_current, source_hashes"
291
+ ).eq("company_id", company_id).order("compiled_at", desc=True).execute()
292
+
293
+ versions = []
294
+ for row in res.data:
295
+ brain_json = None
296
+ # Get skill count from the full row
297
+ full = db.table("skills_files").select("brain_json").eq("id", row["id"]).execute()
298
+ skill_count = 0
299
+ if full.data:
300
+ skill_count = len(full.data[0]["brain_json"].get("skills", []))
301
+ versions.append({
302
+ "id": row["id"],
303
+ "version": row["version"],
304
+ "compiled_at": row["compiled_at"],
305
+ "is_current": row["is_current"],
306
+ "source_count": len(row.get("source_hashes", {})),
307
+ "skill_count": skill_count,
308
+ })
309
+
310
+ return {"versions": versions, "company_id": company_id}
backend/models/schemas.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional, Dict, Any
3
+
4
+ class CompileRequest(BaseModel):
5
+ company_id: str
6
+ force_recompile: bool = False
7
+
8
+ class AgentHandleRequest(BaseModel):
9
+ """Legacy schema — kept for frontend compatibility."""
10
+ company_id: str
11
+ scenario: str
12
+ context: Optional[Dict[str, Any]] = None
13
+ with_brain: bool = True
14
+
15
+ class AgentQueryRequest(BaseModel):
16
+ """New canonical schema for agent queries."""
17
+ company_id: str
18
+ scenario_text: str
19
+ json_context: Optional[Dict[str, Any]] = None
20
+ with_brain: bool = True
backend/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115
2
+ uvicorn[standard]
3
+ openai
4
+ langgraph>=0.4
5
+ sentence-transformers
6
+ numpy
7
+ supabase
8
+ python-dotenv
9
+ python-multipart
10
+ pydantic
backend/sse.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ from typing import Dict, AsyncGenerator
4
+
5
+ class CompilationEventBus:
6
+ def __init__(self):
7
+ self.queues: Dict[str, asyncio.Queue] = {}
8
+
9
+ def get_queue(self, job_id: str) -> asyncio.Queue:
10
+ if job_id not in self.queues:
11
+ self.queues[job_id] = asyncio.Queue()
12
+ return self.queues[job_id]
13
+
14
+ async def emit_event(self, job_id: str, event_type: str, data: dict):
15
+ queue = self.get_queue(job_id)
16
+ await queue.put({"type": event_type, "data": data})
17
+
18
+ async def event_generator(self, job_id: str) -> AsyncGenerator[str, None]:
19
+ """Yields SSE-formatted strings. Uses unnamed events so the
20
+ frontend's EventSource.onmessage handler fires correctly.
21
+ Payload: data: {"event": "<type>", "data": {<payload>}}\n\n
22
+ """
23
+ queue = self.get_queue(job_id)
24
+ try:
25
+ while True:
26
+ event = await asyncio.wait_for(queue.get(), timeout=300)
27
+ payload = json.dumps({"event": event["type"], "data": event["data"]})
28
+ yield f"data: {payload}\n\n"
29
+ if event["type"] in ["pipeline_complete", "pipeline_error"]:
30
+ break
31
+ except asyncio.TimeoutError:
32
+ yield f'data: {json.dumps({"event": "timeout", "data": {}})}\n\n'
33
+ finally:
34
+ if job_id in self.queues:
35
+ del self.queues[job_id]
36
+
37
+ event_bus = CompilationEventBus()
38
+
39
+ async def emit(job_id: str, event_type: str, data: dict):
40
+ await event_bus.emit_event(job_id, event_type, data)
backend/test_compile.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import json
4
+ import uuid
5
+ import sys
6
+ from dotenv import load_dotenv
7
+
8
+ # Set backend in path
9
+ sys.path.append(os.path.dirname(os.path.dirname(__file__)))
10
+
11
+ from backend.graph.graph import build_compilation_graph
12
+
13
+ async def run_compilation_test():
14
+ load_dotenv()
15
+
16
+ # Check vLLM
17
+ vllm_url = os.getenv("VLLM_BASE_URL")
18
+ if not vllm_url:
19
+ print("VLLM_BASE_URL not set in .env. LLM calls will fail.")
20
+ else:
21
+ print(f"Using VLLM_BASE_URL: {vllm_url}")
22
+
23
+ company_id = "rivanly-inc"
24
+ job_id = str(uuid.uuid4())
25
+
26
+ # Read files
27
+ source_files = []
28
+ sources_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "sources")
29
+ if os.path.exists(sources_dir):
30
+ import hashlib
31
+ for filename in os.listdir(sources_dir):
32
+ filepath = os.path.join(sources_dir, filename)
33
+ with open(filepath, "r", encoding="utf-8") as f:
34
+ content = f.read()
35
+
36
+ ftype = "unknown"
37
+ if filename.endswith(".json"):
38
+ if "slack" in filename: ftype = "slack_json"
39
+ elif "tickets" in filename: ftype = "tickets_json"
40
+ elif filename.endswith(".md"):
41
+ ftype = "notion_md"
42
+
43
+ source_files.append({
44
+ "filename": filename,
45
+ "content": content,
46
+ "type": ftype,
47
+ "sha256": hashlib.sha256(content.encode('utf-8')).hexdigest()
48
+ })
49
+ else:
50
+ print(f"No sources dir found at {sources_dir}")
51
+ return
52
+
53
+ print(f"Found {len(source_files)} source files. Starting graph...")
54
+
55
+ initial_state = {
56
+ "job_id": job_id,
57
+ "company_id": company_id,
58
+ "source_files": source_files,
59
+ "structured_sops": [],
60
+ "normalized_events": [],
61
+ "resolved_cases": [],
62
+ "extracted_decisions": [],
63
+ "extracted_workflows": [],
64
+ "extracted_exceptions": [],
65
+ "detected_contradictions": [],
66
+ "skills_file": {}
67
+ }
68
+
69
+ graph = build_compilation_graph()
70
+
71
+ try:
72
+ final_state = await graph.ainvoke(initial_state)
73
+ print("\n=== COMPILATION COMPLETE ===")
74
+ print(f"Extracted Decisions: {len(final_state.get('extracted_decisions', []))}")
75
+ print(f"Detected Contradictions: {len(final_state.get('detected_contradictions', []))}")
76
+ for c in final_state.get('detected_contradictions', []):
77
+ print(f" - Contradiction: {c}")
78
+
79
+ skills_file = final_state.get('skills_file', {})
80
+ skills = skills_file.get('skills', [])
81
+ print(f"Generated Skills: {len(skills)}")
82
+ for s in skills:
83
+ print(f" - {s.get('id')} ({s.get('confidence')} conf)")
84
+
85
+ except Exception as e:
86
+ print(f"Graph execution failed: {e}")
87
+
88
+ if __name__ == "__main__":
89
+ asyncio.run(run_compilation_test())
brand_alchemy_company_brain.html ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <style>
3
+ .ba-root { font-family: var(--font-sans); padding: 1.5rem 0; }
4
+ .section-label { font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-tertiary); margin-bottom: 0.75rem; }
5
+ .pov-block { border-left: 2px solid var(--color-border-secondary); padding: 0.75rem 1rem; margin-bottom: 1.5rem; }
6
+ .pov-block p { margin: 0; font-size: 15px; color: var(--color-text-primary); line-height: 1.6; }
7
+ .pov-block .pov-sub { font-size: 13px; color: var(--color-text-secondary); margin-top: 0.4rem; }
8
+ .meta-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 2rem; }
9
+ .meta-card { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 0.75rem 1rem; }
10
+ .meta-card .mk { font-size: 11px; color: var(--color-text-tertiary); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.06em; }
11
+ .meta-card .mv { font-size: 13px; font-weight: 500; color: var(--color-text-primary); }
12
+ .name-card { background: var(--color-background-primary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); padding: 1.25rem; margin-bottom: 0.75rem; }
13
+ .name-card.winner { border: 1.5px solid #1D9E75; }
14
+ .name-header { display: flex; align-items: center; gap: 12px; margin-bottom: 0.75rem; }
15
+ .name-title { font-size: 22px; font-weight: 500; color: var(--color-text-primary); letter-spacing: -0.02em; }
16
+ .name-score { display: flex; gap: 6px; margin-left: auto; align-items: center; }
17
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-border-tertiary); }
18
+ .dot.filled { background: #1D9E75; }
19
+ .winner-badge { font-size: 11px; font-weight: 500; background: #E1F5EE; color: #0F6E56; padding: 3px 10px; border-radius: 20px; }
20
+ .name-tagline { font-size: 14px; color: var(--color-text-secondary); margin-bottom: 0.75rem; font-style: italic; }
21
+ .phono-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 0.75rem; }
22
+ .phono-pill { font-size: 11px; background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); color: var(--color-text-secondary); padding: 3px 10px; border-radius: 20px; }
23
+ .phono-pill.p { background: #EEEDFE; color: #3C3489; border-color: #AFA9EC; }
24
+ .phono-pill.l { background: #E1F5EE; color: #0F6E56; border-color: #5DCAA5; }
25
+ .phono-pill.f { background: #FAEEDA; color: #633806; border-color: #EF9F27; }
26
+ .name-reasoning { font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; margin-bottom: 0.75rem; }
27
+ .domain-row { display: flex; gap: 8px; flex-wrap: wrap; }
28
+ .domain-tag { font-size: 12px; font-weight: 500; padding: 3px 10px; border-radius: 20px; display: flex; align-items: center; gap: 5px; }
29
+ .domain-tag.available { background: #E1F5EE; color: #085041; }
30
+ .domain-tag.taken { background: #FCEBEB; color: #791F1F; }
31
+ .diamond-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 2rem; }
32
+ .diamond-item { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 0.6rem; text-align: center; }
33
+ .diamond-label { font-size: 11px; color: var(--color-text-tertiary); margin-bottom: 4px; }
34
+ .diamond-bar-bg { height: 4px; background: var(--color-border-tertiary); border-radius: 2px; overflow: hidden; }
35
+ .diamond-bar-fill { height: 4px; border-radius: 2px; background: #1D9E75; }
36
+ .diamond-score { font-size: 12px; font-weight: 500; color: var(--color-text-primary); margin-top: 4px; }
37
+ .divider { border: none; border-top: 0.5px solid var(--color-border-tertiary); margin: 1.5rem 0; }
38
+ .rec-block { background: #E1F5EE; border-radius: var(--border-radius-lg); padding: 1.25rem; margin-top: 1rem; }
39
+ .rec-title { font-size: 14px; font-weight: 500; color: #085041; margin-bottom: 0.5rem; }
40
+ .rec-text { font-size: 13px; color: #0F6E56; line-height: 1.6; }
41
+ .positioning-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 1.5rem; }
42
+ .positioning-table td { padding: 8px 12px; border-bottom: 0.5px solid var(--color-border-tertiary); color: var(--color-text-secondary); vertical-align: top; }
43
+ .positioning-table td:first-child { font-weight: 500; color: var(--color-text-primary); width: 40%; }
44
+ </style>
45
+
46
+ <div class="ba-root">
47
+ <h2 style="sr-only">Brand Alchemy Report — Company Brain</h2>
48
+
49
+ <div class="section-label">Product DNA — what was learned</div>
50
+ <div class="pov-block">
51
+ <p>A compiler layer that extracts the operational judgment scattered across Slack, SOPs, tickets and people's heads — and produces a versioned, evidence-linked, executable skills file any AI agent can consume.</p>
52
+ <p class="pov-sub">Not RAG. Not a chatbot. Not search. A compiler of how a company actually decides things — living, stale-aware, and infrastructure-grade.</p>
53
+ </div>
54
+
55
+ <div class="meta-grid">
56
+ <div class="meta-card">
57
+ <div class="mk">Category</div>
58
+ <div class="mv">AI Infrastructure</div>
59
+ </div>
60
+ <div class="meta-card">
61
+ <div class="mk">Audience</div>
62
+ <div class="mv">B2B SaaS Ops + AI Builders</div>
63
+ </div>
64
+ <div class="meta-card">
65
+ <div class="mk">Brand vibe</div>
66
+ <div class="mv">Authoritative + Precise</div>
67
+ </div>
68
+ <div class="meta-card">
69
+ <div class="mk">Core metaphor</div>
70
+ <div class="mv">Compiler, not assistant</div>
71
+ </div>
72
+ <div class="meta-card">
73
+ <div class="mk">Alternative today</div>
74
+ <div class="mv">Long system prompts, Notion docs</div>
75
+ </div>
76
+ <div class="meta-card">
77
+ <div class="mk">The moat</div>
78
+ <div class="mv">Stale detection + evidence trail</div>
79
+ </div>
80
+ </div>
81
+
82
+ <hr class="divider">
83
+
84
+ <div class="section-label">Category point of view (POV)</div>
85
+ <div class="pov-block" style="margin-bottom: 2rem;">
86
+ <p>The old category — "knowledge management" — is about humans finding information. The new category is <strong style="color:var(--color-text-primary)">operational memory infrastructure</strong>: the persistent, executable layer that lets AI agents inherit a company's judgment. The race isn't between AI models. It's between companies that give their agents operational memory and those that don't.</p>
87
+ </div>
88
+
89
+ <hr class="divider">
90
+
91
+ <div class="section-label">Phonosemantics key — sound symbolism used</div>
92
+ <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom: 1.5rem;">
93
+ <span class="phono-pill p">P — plosive → authority, precision</span>
94
+ <span class="phono-pill l">L — liquid → intelligence, flow</span>
95
+ <span class="phono-pill f">F — fricative → speed, edge</span>
96
+ <span class="phono-pill" style="background:#FAECE7;color:#712B13;border-color:#F0997B;">N — nasal → warmth, connection</span>
97
+ <span class="phono-pill" style="background:#E6F1FB;color:#0C447C;border-color:#85B7EB;">K — hard plosive → technical force</span>
98
+ </div>
99
+
100
+ <div class="section-label">5 name candidates — linguistic breakdown + domain verification</div>
101
+
102
+ <!-- KERNL -->
103
+ <div class="name-card winner">
104
+ <div class="name-header">
105
+ <div class="name-title">Kernl</div>
106
+ <span class="winner-badge">★ top pick</span>
107
+ <div class="name-score">
108
+ <div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div>
109
+ </div>
110
+ </div>
111
+ <div class="name-tagline">"The operational kernel your AI agents run on."</div>
112
+ <div class="phono-row">
113
+ <span class="phono-pill p">K plosive — technical force</span>
114
+ <span class="phono-pill l">R liquid — intelligence</span>
115
+ <span class="phono-pill" style="background:#FAECE7;color:#712B13;border-color:#F0997B;">N nasal — familiar</span>
116
+ <span class="phono-pill" style="background:#E6F1FB;color:#0C447C;border-color:#85B7EB;">dropped 'e' — modern tech register</span>
117
+ </div>
118
+ <div class="name-reasoning">A kernel is the essential core of an operating system — the layer that mediates between hardware and everything above it. Kernl <em>is</em> that layer for AI agents: between raw company data and reliable automation. The dropped final 'e' (à la Tumblr, Flickr) signals a distinctly technical register. Infrastructure-grade naming — belongs alongside Redis, Kafka, Postgres.</div>
119
+ <div class="domain-row">
120
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> kernl.com</span>
121
+ <span class="domain-tag taken"><i class="ti ti-x" aria-hidden="true"></i> kernl.ai</span>
122
+ <span class="domain-tag taken"><i class="ti ti-x" aria-hidden="true"></i> kernl.io</span>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- MELD -->
127
+ <div class="name-card">
128
+ <div class="name-header">
129
+ <div class="name-title">Meld</div>
130
+ <div class="name-score">
131
+ <div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot"></div>
132
+ </div>
133
+ </div>
134
+ <div class="name-tagline">"Melds scattered knowledge into a single executable mind."</div>
135
+ <div class="phono-row">
136
+ <span class="phono-pill" style="background:#FAECE7;color:#712B13;border-color:#F0997B;">M nasal — warmth, familiarity</span>
137
+ <span class="phono-pill l">L liquid — intelligence</span>
138
+ <span class="phono-pill p">D plosive — decisive, final</span>
139
+ <span class="phono-pill" style="background:#E6F1FB;color:#0C447C;border-color:#85B7EB;">1 syllable — maximum compression</span>
140
+ </div>
141
+ <div class="name-reasoning">To meld is to blend disparate elements into a unified whole. In card games, it means to lay down your hidden hand — revealing what was implicit. Both meanings map perfectly: Meld takes scattered, implicit operational knowledge and merges it into explicit, executable form. One syllable, pure infrastructure energy, zero ambiguity.</div>
142
+ <div class="domain-row">
143
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> meld.com</span>
144
+ <span class="domain-tag taken"><i class="ti ti-x" aria-hidden="true"></i> meld.ai</span>
145
+ <span class="domain-tag taken"><i class="ti ti-x" aria-hidden="true"></i> meld.io</span>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- OPSLORE -->
150
+ <div class="name-card">
151
+ <div class="name-header">
152
+ <div class="name-title">Opslore</div>
153
+ <div class="name-score">
154
+ <div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot"></div>
155
+ </div>
156
+ </div>
157
+ <div class="name-tagline">"The living operational lore of your company, made executable."</div>
158
+ <div class="phono-row">
159
+ <span class="phono-pill p">P plosive — operational precision</span>
160
+ <span class="phono-pill f">S fricative — speed</span>
161
+ <span class="phono-pill l">L+R liquids — intelligence, depth</span>
162
+ </div>
163
+ <div class="name-reasoning">Lore is the body of accumulated, traditional knowledge belonging to a group — the unwritten rules, the cultural wisdom. "Opslore" is the operational lore of a company: how refunds get handled, why escalation chains exist, what the pricing exception rule actually is. The word has warmth and depth while remaining precise. Best domain position of any candidate — .com, .ai, and .io all clear.</div>
164
+ <div class="domain-row">
165
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> opslore.com</span>
166
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> opslore.ai</span>
167
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> opslore.io</span>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- OPSCODEX -->
172
+ <div class="name-card">
173
+ <div class="name-header">
174
+ <div class="name-title">Opscodex</div>
175
+ <div class="name-score">
176
+ <div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot"></div>
177
+ </div>
178
+ </div>
179
+ <div class="name-tagline">"The compiled operational codex your agents execute from."</div>
180
+ <div class="phono-row">
181
+ <span class="phono-pill p">K+P plosives — double authority</span>
182
+ <span class="phono-pill" style="background:#EEEDFE;color:#3C3489;border-color:#AFA9EC;">X terminal — distinct, rare</span>
183
+ <span class="phono-pill" style="background:#E6F1FB;color:#0C447C;border-color:#85B7EB;">Codex etymology — compiled law</span>
184
+ </div>
185
+ <div class="name-reasoning">A codex was the ancient form of the book — sheets compiled and bound, replacing scrolls. Historically, codices preserved legal codes, canonical texts, laws. Opscodex is the compiled operational code of a company: the canonical, authoritative record of how things are decided. The terminal X adds sonic distinctiveness and technical sharpness. Carries scholarly gravitas — infrastructure, not a feature.</div>
186
+ <div class="domain-row">
187
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> opscodex.com</span>
188
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> opscodex.ai</span>
189
+ </div>
190
+ </div>
191
+
192
+ <!-- LOREKERN -->
193
+ <div class="name-card">
194
+ <div class="name-header">
195
+ <div class="name-title">Lorekern</div>
196
+ <div class="name-score">
197
+ <div class="dot filled"></div><div class="dot filled"></div><div class="dot filled"></div><div class="dot"></div><div class="dot"></div>
198
+ </div>
199
+ </div>
200
+ <div class="name-tagline">"The kernel of operational lore, distilled and executable."</div>
201
+ <div class="phono-row">
202
+ <span class="phono-pill l">L+R liquids — intelligence, flow</span>
203
+ <span class="phono-pill p">K plosive — technical core</span>
204
+ <span class="phono-pill" style="background:#FAECE7;color:#712B13;border-color:#F0997B;">N nasal — grounding</span>
205
+ </div>
206
+ <div class="name-reasoning">Morpheme blend of Lore (accumulated operational wisdom) and Kern (kernel/core). Reads as two concepts merged — like the product itself: taking the living lore of an organization and distilling it into an executable core. More descriptive and compound than the other candidates; works well if a human-warmth brand angle is preferred over pure infrastructure framing.</div>
207
+ <div class="domain-row">
208
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> lorekern.com</span>
209
+ <span class="domain-tag available"><i class="ti ti-check" aria-hidden="true"></i> lorekern.ai</span>
210
+ </div>
211
+ </div>
212
+
213
+ <hr class="divider">
214
+
215
+ <div class="section-label">Diamond test — top pick: Kernl</div>
216
+ <div class="diamond-grid">
217
+ <div class="diamond-item">
218
+ <div class="diamond-label">Distinctiveness</div>
219
+ <div class="diamond-bar-bg"><div class="diamond-bar-fill" style="width:96%"></div></div>
220
+ <div class="diamond-score">96</div>
221
+ </div>
222
+ <div class="diamond-item">
223
+ <div class="diamond-label">Processing fluency</div>
224
+ <div class="diamond-bar-bg"><div class="diamond-bar-fill" style="width:95%"></div></div>
225
+ <div class="diamond-score">95</div>
226
+ </div>
227
+ <div class="diamond-item">
228
+ <div class="diamond-label">Relevance</div>
229
+ <div class="diamond-bar-bg"><div class="diamond-bar-fill" style="width:97%"></div></div>
230
+ <div class="diamond-score">97</div>
231
+ </div>
232
+ <div class="diamond-item">
233
+ <div class="diamond-label">Energy</div>
234
+ <div class="diamond-bar-bg"><div class="diamond-bar-fill" style="width:88%"></div></div>
235
+ <div class="diamond-score">88</div>
236
+ </div>
237
+ </div>
238
+
239
+ <div class="rec-block">
240
+ <div class="rec-title">Final recommendation: Kernl</div>
241
+ <div class="rec-text">Register <strong>kernl.com</strong> immediately. The kernel metaphor is structurally perfect — it is the deepest, most precise analogy in the OS/infra vocabulary for what this product does. The hard K plosive delivers maximum technical authority. The dropped 'e' places it firmly in infrastructure naming tradition. It scales: "your company's Kernl", "deploy Kernl", "the Kernl API". It competes with Redis and Kafka on naming gravity, which is exactly the positioning the PRD demands.</div>
242
+ </div>
243
+
244
+ <hr class="divider">
245
+
246
+ <div class="section-label">Visual system direction</div>
247
+ <table class="positioning-table">
248
+ <tr><td>Mark style</td><td>Wordmark only. No icon. Infrastructure products don't need icons — they are the icon. Monospace or geometric sans. Think Stripe, Linear, Vercel.</td></tr>
249
+ <tr><td>Color</td><td>Single accent on near-black ground. Deep teal (#0F6E56) or electric indigo — evokes precision and living systems without the generic "AI blue" trap.</td></tr>
250
+ <tr><td>Voice</td><td>Declarative. Short sentences. Never explain — demonstrate. "12 skills. 58 seconds. Evidence-linked." — not "our platform leverages AI to..."</td></tr>
251
+ <tr><td>Tagline direction</td><td>"Operational memory for AI agents." — or — "Your AI knows how your company works."</td></tr>
252
+ <tr><td>Pitch one-liner</td><td>"Kernl compiles how your company decides into an executable skills file. Any agent. Any task. Correct every time."</td></tr>
253
+ </table>
254
+ </div>
company_brain_PRD_v4.md ADDED
@@ -0,0 +1,1061 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Company Brain — Product Requirements Document
2
+ **Version:** 4.0 — Final (Pre-Build, All Issues Resolved)
3
+ **Date:** May 4, 2026
4
+ **Authors:** Abhijith Pingali, Harshit Anand
5
+ **Status:** Final — Build starts post-kickoff
6
+
7
+ > **v4 changes over v3:**
8
+ > 1. Ground truth table completed — all 12 Rivanly scenarios with expected action + skill
9
+ > 2. `with_brain: false` behaviour fully documented in Section 9
10
+ > 3. Section 10 user flow added — screen-to-screen navigation with decision points
11
+ > 4. Competitive landscape updated with 8 real companies identified in LinkedIn thread
12
+ > 5. Risk table updated: "knowledge never captured" risk added from Paul Breuler's comment
13
+ > 6. Section 2.5 added: "The Stale Knowledge Problem" — validates drift detection as core feature
14
+ > 7. Section 15 updated: execution boundary insight from Horizon Labs added to v2 roadmap
15
+
16
+ ---
17
+
18
+ ## 1. Executive Summary
19
+
20
+ **Problem:** AI agents deployed by B2B companies behave like a new hire on day one — they lack the operational judgment embedded in how the company actually decides things. This knowledge lives in Slack threads, SOPs, support tickets, and people's heads, invisible to any model.
21
+
22
+ **Solution:** Company Brain is a compilation layer that extracts this operational judgment and produces a versioned, evidence-linked, executable skills file any AI agent can consume to act like the company's best employee.
23
+
24
+ **Success Criteria (Hackathon v0):**
25
+
26
+ | KPI | Target |
27
+ |---|---|
28
+ | Full compilation pipeline: sources → 12 skills | Completes without error, every run |
29
+ | Skills with confidence ≥ 0.7 | ≥ 10 of 12 |
30
+ | Brain agent: correct action on all Rivanly scenarios | 12 / 12 correct |
31
+ | Compilation time on AMD MI300X | < 90s (target 60s) |
32
+ | Brain agent response latency | < 8s per query |
33
+
34
+ ---
35
+
36
+ ## 2. Problem Statement & Solution
37
+
38
+ ### 2.1 The Problem
39
+
40
+ Every company trying to deploy AI automation hits the same wall. The models are good enough. The infrastructure is available. But the AI behaves like a new hire on day one — it doesn't know how the company actually operates.
41
+
42
+ Refund policies live in Priya's head. Pricing exceptions get decided in Slack threads nobody archived. Escalation chains exist because three incidents taught the team the hard way. This operational knowledge — how the company actually decides things — is invisible to AI agents.
43
+
44
+ Existing solutions miss this entirely. RAG retrieves document chunks. Chatbots answer questions. Neither gives an AI agent the operational judgment to do real work correctly and consistently.
45
+
46
+ ### 2.2 The Solution
47
+
48
+ Company Brain is the missing compilation layer. It extracts the operational judgment embedded in how a company behaves — not what it documents, but how it actually decides — and compiles it into an executable, versioned, living skills file that any AI agent can use.
49
+
50
+ **Agents are compilers, not assistants.** Company Brain's extraction agents do not summarize or search. They convert messy human behavior into structured, executable logic. The downstream brain agent that does real work is a consumer of that compiled output — it never reasons from scratch.
51
+
52
+ ### 2.3 One-Line Pitch
53
+
54
+ > "We turn how your company actually operates into an executable Company Brain. Any agent can use it to do real work without guessing."
55
+
56
+ ### 2.4 Product Positioning
57
+
58
+ Company Brain is **infrastructure, not a feature.**
59
+
60
+ | What it is NOT | What it IS |
61
+ |---|---|
62
+ | RAG over documents | Compiler of operational judgment |
63
+ | Chatbot over your data | Executable skills file for AI agents |
64
+ | A search engine | A living map of how your company works |
65
+ | One-time snapshot | Versioned, updatable, drift-aware |
66
+
67
+ ### 2.5 The Stale Knowledge Problem (Why "Living" Matters)
68
+
69
+ *Validated by multiple practitioners in the YC RFS LinkedIn thread.*
70
+
71
+ The hardest part of any knowledge system is not building it — it is keeping it alive. Most companies will document their workflows once, ship the AI agent, and within six weeks the map diverges from reality. A new pricing exception gets approved in a Slack DM. An escalation chain changes when someone leaves. The AI keeps following the old rules.
72
+
73
+ Company Brain's stale detection — SHA-256 hashing of source files, `stale: true` badges on affected skills, and recompile triggers — directly solves this. The skills file is not a document. It is a living artifact that stays current with how the company actually evolves.
74
+
75
+ This is not a minor feature. It is the moat.
76
+
77
+ ### 2.6 What Company Brain Does NOT Solve (v0)
78
+
79
+ *Acknowledged risk from Paul Breuler (BaseState founder), LinkedIn thread:*
80
+
81
+ > "The decisions that matter happen in context, on the ground, and were never captured in a ticket or Slack thread."
82
+
83
+ Company Brain compiles knowledge that was captured somewhere — Slack, SOPs, tickets, call transcripts. Knowledge that exists only in someone's head and was never written down or discussed in any recorded channel cannot be extracted. This is a known limitation. The pitch should never claim to capture all company knowledge — only the knowledge that was communicated in any digital form.
84
+
85
+ For v1, voice call transcription addresses a portion of this gap. For v2, an active knowledge capture interface (where employees record decisions as they happen) closes it further.
86
+
87
+ ---
88
+
89
+ ## 3. Target Customer & User Personas
90
+
91
+ ### 3.1 Primary Wedge — v1
92
+
93
+ **B2B SaaS companies, 10–50 employees, actively deploying AI automation for the first time.**
94
+
95
+ These companies have:
96
+ - Enough operational complexity that AI agents fail without context
97
+ - Enough technical sophistication to understand why RAG isn't solving their problem
98
+ - Enough urgency to pay for a solution (they are actively trying and failing to deploy AI)
99
+ - Not enough resources to build this infrastructure themselves
100
+
101
+ ### 3.2 User Personas
102
+
103
+ | Persona | Role | Primary Goal | Pain Point |
104
+ |---|---|---|---|
105
+ | **Ops Owner** | Head of Operations / Founder | Get AI agents to handle work consistently | Agents hallucinate edge cases; policies go stale |
106
+ | **AI Builder** | Developer / Automation Lead | Consume company knowledge in agent prompts | No structured source of truth to inject into prompts |
107
+ | **Agent Consumer** | Support agent, AM, anyone using the AI | Get correct, policy-backed responses instantly | Generic AI responses that don't match company policy |
108
+ | **Demo Viewer / Judge** | Hackathon judge, investor, prospect | Understand what the product does in 5 minutes | Can't distinguish this from "just another RAG tool" |
109
+
110
+ ### 3.3 Fictional Reference Customer — Rivanly Inc.
111
+
112
+ Rivanly is a 15-person B2B SaaS company used throughout this document and the demo. 6 departments, 12 operational skills. Enough complexity to make the product real, small enough to demo in 5 minutes.
113
+
114
+ ### 3.4 Expanded Customer Universe — v2+
115
+
116
+ - E-commerce operators (refund, shipping, returns automation)
117
+ - Agencies (client approval, scope change, billing exception workflows)
118
+ - Healthcare admin (referral routing, prior authorization, scheduling exceptions)
119
+ - Legal operations (intake, escalation, matter routing)
120
+
121
+ ---
122
+
123
+ ## 4. Jobs To Be Done
124
+
125
+ | Job | Current Solution | Problem |
126
+ |---|---|---|
127
+ | "I want an AI agent to handle customer refunds correctly" | Write a long system prompt with refund rules | Rules go stale, edge cases missed, no evidence trail |
128
+ | "I need to onboard a new AI tool to how we operate" | Document everything manually in Notion | Takes weeks, immediately outdated, agent still hallucinates |
129
+ | "I want to know if my AI agent is following company policy" | Read agent logs manually | No structured audit trail linking actions to rules |
130
+ | "We updated our pricing policy — the AI needs to know" | Edit the system prompt manually | No systematic way to detect or propagate policy changes |
131
+ | "Why did the agent make that decision?" | Cannot answer | No evidence chain from agent action back to source |
132
+
133
+ ---
134
+
135
+ ## 5. User Stories
136
+
137
+ ### Source Ingestion & File Handling
138
+
139
+ 1. As an Ops Owner, I want to upload `.md` Notion SOPs so that my written policies are ingested without manual reformatting.
140
+ 2. As an Ops Owner, I want to upload Slack JSON exports so that informal decision patterns from real conversations are captured.
141
+ 3. As an Ops Owner, I want to upload Zendesk ticket JSON exports so that resolved case reasoning is extracted as evidence.
142
+ 4. As an Ops Owner, I want the system to detect unchanged files by SHA-256 hash so that re-uploading a file doesn't trigger unnecessary re-extraction.
143
+ 5. As an Ops Owner, I want a clear parse error message when a file is malformed so that I know exactly which file to fix.
144
+ 6. As an Ops Owner, I want unsupported file types to be rejected with a helpful error so that I don't wait for a compilation that will fail.
145
+ 7. As an Ops Owner, I want source files stored in Supabase so that I don't need to re-upload them on every compile.
146
+
147
+ ### Compilation Pipeline
148
+
149
+ 8. As an Ops Owner, I want 4 extraction agents to run in parallel on AMD MI300X so that compilation completes under 90 seconds instead of 8+ minutes.
150
+ 9. As an Ops Owner, I want IF-THEN-EXCEPT decision rules extracted from Slack threads so that informal decisions become structured, executable policies.
151
+ 10. As an Ops Owner, I want sequential process steps extracted from SOPs and runbooks so that workflow sequences are captured correctly.
152
+ 11. As an Ops Owner, I want edge cases, overrides, and "unless..." patterns extracted specifically so that exception logic isn't lost in summarization.
153
+ 12. As an Ops Owner, I want contradictions between SOPs and actual Slack/ticket behavior flagged so that I can identify and resolve policy drift.
154
+ 13. As an Ops Owner, I want skills with confidence below 0.6 to be flagged for human review rather than auto-published so that only verified rules go live.
155
+ 14. As an Ops Owner, I want each decision rule backlinked to its source file and excerpt so that every policy is auditable, not asserted.
156
+ 15. As an Ops Owner, I want the system to retry once if the LLM returns malformed JSON so that a single bad LLM response doesn't abort the whole compile.
157
+ 16. As an Ops Owner, I want a clear compile error message if vLLM becomes unreachable so that I know the issue is infrastructure, not my data.
158
+ 17. As a Developer, I want LangGraph checkpointing via MemorySaver so that a crashed compile can be inspected and does not silently lose data.
159
+
160
+ ### Skills File & Schema
161
+
162
+ 18. As an Ops Owner, I want each skill stored as a versioned JSON object with id, name, domain, confidence, decision_logic, forbidden_actions, escalation_chain, and evidence_sources so that the schema is complete and consistent.
163
+ 19. As an Ops Owner, I want skills converted to markdown at query time so that they are injected into LLM prompts efficiently.
164
+ 20. As an Ops Owner, I want the meta block of the skills file to store source hashes so that stale skills can be detected when source files change.
165
+ 21. As an Ops Owner, I want the skills file versioned with semver so that every compile produces a traceable snapshot.
166
+
167
+ ### Version Management & Drift Detection
168
+
169
+ 22. As an Ops Owner, I want to compare any two historical brain versions in a diff view so that I can see exactly what changed after a policy update.
170
+ 23. As an Ops Owner, I want changed rules highlighted in the diff (added green, removed red, modified yellow) so that I don't have to read everything to spot changes.
171
+ 24. As an Ops Owner, I want stale skills badged in the Skills Viewer so that I know which skills need recompilation after a source file changed.
172
+ 25. As an Ops Owner, I want at least 2 pre-seeded historical versions in the demo so that the diff view is usable on day one.
173
+
174
+ ### Brain Dashboard (Frontend)
175
+
176
+ 26. As an Ops Owner, I want a "Build Company Brain" button on the dashboard so that I can trigger recompilation from the UI without touching a terminal.
177
+ 27. As an Ops Owner, I want the button to be disabled with a spinner during an active compile so that I can't trigger duplicate jobs.
178
+ 28. As an Ops Owner, I want a real-time SSE feed showing each pipeline node completing with timestamps so that I trust the system is working.
179
+ 29. As an Ops Owner, I want the compilation time displayed to the second so that I can use this as a live AMD MI300X proof point.
180
+ 30. As an Ops Owner, I want the current brain version and last-compiled timestamp visible at a glance so that I know which brain is active.
181
+
182
+ ### Skills Viewer (Frontend)
183
+
184
+ 31. As an Ops Owner, I want skills grouped by department so that I can navigate to the right area quickly.
185
+ 32. As an Ops Owner, I want a visual confidence bar per skill so that I can immediately see which skills are strong vs. uncertain without reading numbers.
186
+ 33. As an Ops Owner, I want to expand any skill and see all its decision conditions and forbidden actions so that I can verify the rules are correct.
187
+ 34. As an Ops Owner, I want the evidence panel per skill to show source file names and excerpts so that I can trace every policy back to where it came from.
188
+
189
+ ### Brain Agent (Demo)
190
+
191
+ 35. As an AI Builder, I want to submit a natural language scenario to the brain agent so that I get a structured action recommendation with evidence, not a guess.
192
+ 36. As an AI Builder, I want the response to include the exact rule condition that matched so that I can verify the logic is correct.
193
+ 37. As an AI Builder, I want the agent to gracefully handle scenarios with no matching skill so that low-confidence situations escalate to a human rather than produce a wrong answer.
194
+ 38. As a Demo Viewer / Judge, I want to see the agent without the brain respond generically to the same scenario so that the value of the compilation layer is immediately obvious.
195
+ 39. As a Demo Viewer / Judge, I want to see the "Change a SOP rule → Rebuild → same scenario → different outcome" flow so that I understand this is a living map, not a static snapshot.
196
+
197
+ ---
198
+
199
+ ## 6. Product Scope
200
+
201
+ ### 6.1 Three-Ring Model
202
+
203
+ | Ring | Name | Timeline | What Ships |
204
+ |---|---|---|---|
205
+ | **Ring 1** | Hackathon v0 | May 4–10, 2026 | Offline compiler, Rivanly demo, file upload inputs, brain agent demo |
206
+ | **Ring 2** | Product v1 | 4–6 weeks post-hackathon | Live connectors, multi-tenant, real company data, auth |
207
+ | **Ring 3** | Scale | 2–6 months | Agent SDK, skills marketplace, audit trails, RBAC |
208
+
209
+ ### 6.2 Hackathon v0 — In Scope
210
+
211
+ - Multi-agent compilation pipeline (LangGraph, 4 parallel async extraction agents)
212
+ - 6-department, 12-skill coverage of Rivanly Inc.
213
+ - Synthetic dataset (8 source files authored before kickoff)
214
+ - Skills file: JSON storage, markdown runtime, evidence-linked, confidence-scored, versioned
215
+ - Brain agent: scenario input → in-memory skill match → structured response with rule trace
216
+ - Frontend: Brain Dashboard + Skills Viewer + Demo Agent panel
217
+ - Real-time SSE compilation progress feed
218
+ - Brain version diffing (v1.2 → v1.3 what changed)
219
+ - AMD MI300X deployment via vLLM (`RedHatAI/Qwen2.5-72B-Instruct-FP8-dynamic`)
220
+ - Side-by-side "with brain vs. without brain" comparison panel — **P0, the money shot**
221
+ - Build in Public: 2 posts on X/LinkedIn during build
222
+
223
+ ### 6.3 Out of Scope for v0
224
+
225
+ - Real Slack, Notion, Zendesk OAuth connectors — file upload only
226
+ - Multi-tenant isolation — single company demo
227
+ - Auth / login — none required for demo
228
+ - Redis job queue — direct `graph.ainvoke()` only
229
+ - pgvector — in-memory sentence-transformers for v0 skill matching
230
+ - Webhook-triggered recompilation
231
+ - Human skill review queue UI
232
+
233
+ ### 6.4 Team Ownership
234
+
235
+ | Owner | Scope |
236
+ |---|---|
237
+ | **Abhijith** | F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-12 (pipeline + API) |
238
+ | **Harshit** | F-08, F-09, F-10, F-11 (all frontend) |
239
+ | **Both** | Synthetic dataset — 4 files each, done before May 4 kickoff |
240
+
241
+ ---
242
+
243
+ ## 7. Feature Requirements
244
+
245
+ Priority: **[P0]** = demo breaks without it · **[P1]** = must ship · **[P2]** = ship if time allows
246
+
247
+ ---
248
+
249
+ ### F-01: Source Ingestion [P0]
250
+
251
+ **Functional Requirements:**
252
+ - Accept `.md`, `.json`, `.txt` file uploads
253
+ - Parse Notion SOP markdown → `structured_sops[]`
254
+ - Parse Slack export JSON → `normalized_events[]`
255
+ - Parse ticket JSON → `resolved_cases[]`
256
+ - Compute SHA-256 hash per file; compare to previous run; skip unchanged files
257
+ - No LLM calls at this stage — pure Python parsing only
258
+
259
+ **Acceptance Criteria:**
260
+
261
+ *AC-01-1:*
262
+ - **Given** a valid Notion SOP `.md` file is uploaded
263
+ - **When** the ingest node runs
264
+ - **Then** `structured_sops` contains at least one entry with `source`, `content`, and `type` fields
265
+
266
+ *AC-01-2:*
267
+ - **Given** a file was uploaded in a previous compile with hash `H`
268
+ - **When** the same file is uploaded again unchanged
269
+ - **Then** the ingestion node skips extraction for that file and logs "hash match, skipping"
270
+
271
+ *AC-01-3:*
272
+ - **Given** a malformed JSON file is uploaded
273
+ - **When** the ingest node attempts to parse it
274
+ - **Then** the SSE stream emits `node_error` with `file`, `error: "parse_error"`, and `detail` — and the compile continues with remaining files
275
+
276
+ *AC-01-4:*
277
+ - **Given** an unsupported file type (e.g. `.xlsx`) is uploaded
278
+ - **When** `POST /sources/upload` is called
279
+ - **Then** the API returns `400` with `{"error": "unsupported_file_type", "accepted": [".md", ".json", ".txt"]}`
280
+
281
+ ---
282
+
283
+ ### F-02: Parallel Extraction [P0]
284
+
285
+ **Functional Requirements:**
286
+ - Four async LangGraph nodes run simultaneously via `Send` API + `await llm.ainvoke()`
287
+ - Decision Extractor: IF-THEN-EXCEPT judgment patterns from Slack + tickets
288
+ - Workflow Extractor: sequential process steps from SOPs and runbooks
289
+ - Exception Extractor: edge cases, overrides, "unless..." patterns
290
+ - Contradiction Detector: divergence between SOPs and actual behavior
291
+ - All four target `RedHatAI/Qwen2.5-72B-Instruct-FP8-dynamic` on AMD MI300X via vLLM
292
+
293
+ **Acceptance Criteria:**
294
+
295
+ *AC-02-1:*
296
+ - **Given** all three ingest nodes have completed
297
+ - **When** `route_to_extractors` is called
298
+ - **Then** all four extraction nodes start within 2 seconds of each other
299
+
300
+ *AC-02-2:*
301
+ - **Given** the Rivanly synthetic dataset
302
+ - **When** extraction completes
303
+ - **Then** each extractor returns a non-empty list; `contradictions[]` contains ≥ 1 entry
304
+
305
+ *AC-02-3:*
306
+ - **Given** the LLM returns malformed JSON for one extractor
307
+ - **When** the node catches the error
308
+ - **Then** it retries once with a stricter JSON-only prompt; if still malformed, returns empty list and emits `node_error` without aborting other extractors
309
+
310
+ *AC-02-4:*
311
+ - **Given** all four async extractors complete
312
+ - **When** wall clock is checked
313
+ - **Then** total extraction time is under 45 seconds on MI300X
314
+
315
+ ---
316
+
317
+ ### F-03: Skill Compilation [P0]
318
+
319
+ **Functional Requirements:**
320
+ - Synthesize extractor outputs into 12 canonical skill objects
321
+ - Evidence linker: backfill `evidence_sources[]` for every `decision_logic` entry
322
+ - Confidence scorer: `f(source_count, source_recency, internal_consistency)`
323
+ - Skills below 0.6 confidence: present with `"review_required": true`, not auto-published
324
+ - Write `skills_file.json` to Supabase `skills_files` table with incremented semver
325
+
326
+ **Acceptance Criteria:**
327
+
328
+ *AC-03-1:*
329
+ - **Given** all extraction nodes have produced output
330
+ - **When** `synthesize_skills` runs
331
+ - **Then** output contains exactly 12 skill objects, each with all required schema fields
332
+
333
+ *AC-03-2:*
334
+ - **Given** a skill has been synthesized
335
+ - **When** `link_evidence` runs
336
+ - **Then** every `decision_logic` entry has at least one `evidence_sources` entry with non-empty `source` and `excerpt`
337
+
338
+ *AC-03-3:*
339
+ - **Given** a skill has only one supporting source
340
+ - **When** `score_confidence` runs
341
+ - **Then** that skill's `confidence` is below 0.7 and `review_required` is `true` if below 0.6
342
+
343
+ *AC-03-4:*
344
+ - **Given** compilation succeeds
345
+ - **When** `write_brain` runs
346
+ - **Then** `skills_files` table gains a new row with semver one minor bump higher, `is_current: true`, and all previous rows `is_current: false`
347
+
348
+ ---
349
+
350
+ ### F-04: Skills File Format [P0]
351
+
352
+ **Schema (per skill):**
353
+ ```json
354
+ {
355
+ "id": "handle_refund_request",
356
+ "name": "Handle Refund Request",
357
+ "domain": "support",
358
+ "version": "1.2",
359
+ "confidence": 0.91,
360
+ "stale": false,
361
+ "review_required": false,
362
+ "last_updated": "2026-05-04T09:30:00Z",
363
+ "trigger": {
364
+ "phrases": ["refund", "money back"],
365
+ "conditions": ["customer mentions payment dissatisfaction"]
366
+ },
367
+ "decision_logic": [
368
+ {
369
+ "condition": "plan == 'annual' AND days_since_purchase <= 14",
370
+ "action": "approve_full_refund",
371
+ "note": "No-questions policy within 14 days.",
372
+ "evidence_sources": [
373
+ { "source": "notion_refund_sop.md", "excerpt": "...", "confidence": 0.95 }
374
+ ]
375
+ }
376
+ ],
377
+ "forbidden_actions": ["Never process refunds for lifetime deal accounts"],
378
+ "escalation_chain": ["support_agent", "support_lead", "account_manager", "founder"],
379
+ "sla": "respond_within_2h, resolve_within_24h"
380
+ }
381
+ ```
382
+
383
+ **Acceptance Criteria:**
384
+
385
+ *AC-04-1:*
386
+ - **Given** the compiled skills file
387
+ - **When** validated against JSON schema
388
+ - **Then** zero validation errors
389
+
390
+ *AC-04-2:*
391
+ - **Given** a skill selected for prompt injection
392
+ - **When** converted to markdown
393
+ - **Then** output is plain English, contains all conditions and forbidden actions, under 800 tokens
394
+
395
+ ---
396
+
397
+ ### F-05: Brain Version Management [P1]
398
+
399
+ **Acceptance Criteria:**
400
+
401
+ *AC-05-1:*
402
+ - **Given** one source file changed and recompilation triggered
403
+ - **When** compile finishes
404
+ - **Then** new brain version is a minor bump (`1.2.0 → 1.3.0`) and diff endpoint returns that file's dependent skills as `modified_skills`
405
+
406
+ *AC-05-2:*
407
+ - **Given** two brain versions exist
408
+ - **When** `GET /diff/1.2.0/1.3.0` is called
409
+ - **Then** response contains `added_skills`, `removed_skills`, and `modified_skills` with per-skill field-level changes
410
+
411
+ *AC-05-3:*
412
+ - **Given** a source file changes
413
+ - **When** the new compile runs
414
+ - **Then** skills whose `evidence_sources` reference that file have `stale: true`
415
+
416
+ ---
417
+
418
+ ### F-06: Scenario Handling — Brain Agent [P0]
419
+
420
+ **Functional Requirements:**
421
+ - Accept natural language scenario input + optional structured context
422
+ - Embed query via `all-MiniLM-L6-v2` (in-memory, CPU); pre-compute skill embeddings once at startup
423
+ - Cosine similarity match → select top skill
424
+ - Convert skill JSON → markdown snippet
425
+ - Single LLM call: company context + skill rules + scenario
426
+ - Return structured response (F-07)
427
+
428
+ **Acceptance Criteria:**
429
+
430
+ *AC-06-1:*
431
+ - **Given** an enterprise refund scenario
432
+ - **When** `POST /agent/handle` is called
433
+ - **Then** matched skill is `handle_refund_request` (cosine similarity > 0.6)
434
+
435
+ *AC-06-2:*
436
+ - **Given** all 12 Rivanly demo scenarios submitted in sequence
437
+ - **When** each response reviewed
438
+ - **Then** all 12 return correct action (verified against ground truth table in Section 12)
439
+
440
+ *AC-06-3:*
441
+ - **Given** a scenario matching no skill above cosine 0.4
442
+ - **When** match function runs
443
+ - **Then** response is `{"action": "escalate_to_human", "reason": "no_skill_match", "confidence": <score>}` — not an error, not a hallucination
444
+
445
+ *AC-06-4:*
446
+ - **Given** a valid scenario submitted
447
+ - **When** response returned
448
+ - **Then** wall-clock latency under 8 seconds
449
+
450
+ ---
451
+
452
+ ### F-07: Response Structure [P0]
453
+
454
+ Every `POST /agent/handle` response:
455
+
456
+ ```json
457
+ {
458
+ "action": "escalate_to_am_within_1hr",
459
+ "message_to_customer": "...",
460
+ "rule_applied": "plan == 'enterprise' AND any_amount",
461
+ "evidence": {
462
+ "source": "slack_thread_2024-03-12",
463
+ "excerpt": "enterprise = always AM"
464
+ },
465
+ "skill_matched": "handle_refund_request",
466
+ "confidence": 0.91
467
+ }
468
+ ```
469
+
470
+ **Acceptance Criteria:**
471
+
472
+ *AC-07-1:*
473
+ - **Given** any valid scenario input
474
+ - **When** brain agent responds
475
+ - **Then** all six top-level fields present and non-null
476
+
477
+ *AC-07-2:*
478
+ - **Given** the response `rule_applied` field
479
+ - **When** compared to matched skill's `decision_logic`
480
+ - **Then** string matches an exact `condition` field — never paraphrased
481
+
482
+ ---
483
+
484
+ ### F-08: Brain Dashboard [P0]
485
+
486
+ **Acceptance Criteria:**
487
+
488
+ *AC-08-1:*
489
+ - **Given** the dashboard is loaded
490
+ - **When** user clicks "Build Company Brain"
491
+ - **Then** button becomes disabled with spinner and SSE feed begins showing node events within 2 seconds
492
+
493
+ *AC-08-2:*
494
+ - **Given** SSE stream is active
495
+ - **When** each pipeline node completes
496
+ - **Then** feed shows node name, completion checkmark, and elapsed time in real time without page refresh
497
+
498
+ *AC-08-3:*
499
+ - **Given** compilation completes
500
+ - **When** dashboard updates
501
+ - **Then** brain version, last-compiled timestamp, and total compilation time displayed with correct values
502
+
503
+ ---
504
+
505
+ ### F-09: Skills Viewer [P0]
506
+
507
+ **Acceptance Criteria:**
508
+
509
+ *AC-09-1:*
510
+ - **Given** a compiled brain with 12 skills
511
+ - **When** Skills Viewer loaded
512
+ - **Then** all 12 skills visible, grouped under 6 correct department headings
513
+
514
+ *AC-09-2:*
515
+ - **Given** a skill with `stale: true`
516
+ - **When** it appears in the viewer
517
+ - **Then** it has a visible "Stale" badge and confidence bar is visually de-emphasized
518
+
519
+ *AC-09-3:*
520
+ - **Given** user clicks any skill
521
+ - **When** detail panel expands
522
+ - **Then** all `decision_logic` conditions, all `forbidden_actions`, `escalation_chain`, and at least one `evidence_sources` entry visible
523
+
524
+ ---
525
+
526
+ ### F-10: Demo Agent Panel [P0] — Side-by-Side Required
527
+
528
+ **Functional Requirements:**
529
+ - Free-text scenario input + optional structured context fields
530
+ - Two response panels rendered simultaneously:
531
+ - **Without Brain:** Same LLM, same scenario, system prompt contains only the raw scenario — no company name, no skills context, no Rivanly-specific information. Goal: produce a demonstrably generic response.
532
+ - **With Brain:** Full skill context + rule trace + evidence
533
+ - Visual trace showing matched skill and cosine similarity score
534
+
535
+ **Acceptance Criteria:**
536
+
537
+ *AC-10-1:*
538
+ - **Given** a scenario submitted
539
+ - **When** both panels render
540
+ - **Then** both responses appear within 10 seconds
541
+
542
+ *AC-10-2:*
543
+ - **Given** the enterprise refund demo scenario
544
+ - **When** both panels render
545
+ - **Then** "Without Brain" response is generic (no company-specific rule, no evidence); "With Brain" response includes `rule_applied` and `evidence` visually highlighted
546
+
547
+ *AC-10-3:*
548
+ - **Given** a judge views the demo panel for the first time
549
+ - **When** they read both responses
550
+ - **Then** the value of the brain is legible without any verbal explanation
551
+
552
+ ---
553
+
554
+ ### F-11: Version Diff View [P1]
555
+
556
+ **Acceptance Criteria:**
557
+
558
+ *AC-11-1:*
559
+ - **Given** two pre-seeded brain versions (v1.1.0 and v1.2.0)
560
+ - **When** diff view opened and both selected
561
+ - **Then** modified skills highlighted yellow with field-level changes inline
562
+
563
+ *AC-11-2:*
564
+ - **Given** the "change a rule → rebuild → diff" demo flow
565
+ - **When** performed end-to-end
566
+ - **Then** diff correctly shows changed rule in under 30 seconds of demo time
567
+
568
+ ---
569
+
570
+ ### F-12: API Layer [P0]
571
+
572
+ **`POST /compile`**
573
+ ```
574
+ Request: { "company_id": "rivanly-inc", "force_recompile": false }
575
+ Response: { "job_id": "uuid", "status": "started", "stream_url": "/compile/stream?job_id=uuid" }
576
+ ```
577
+
578
+ **`GET /brain/status`**
579
+ ```
580
+ Response: {
581
+ "company_id": "rivanly-inc",
582
+ "brain_version": "1.3.0",
583
+ "last_compiled_at": "2026-05-04T09:30:00Z",
584
+ "total_skills": 12,
585
+ "stale_skills": 2,
586
+ "coverage_areas": ["support", "revenue", "product_eng", "customer_success", "hr", "finance_ops"]
587
+ }
588
+ ```
589
+
590
+ **`GET /skills`**
591
+ ```
592
+ Response: {
593
+ "skills": [
594
+ { "id": "handle_refund_request", "name": "Handle Refund Request",
595
+ "domain": "support", "confidence": 0.91, "stale": false, "version": "1.2" }
596
+ ]
597
+ }
598
+ ```
599
+
600
+ **`GET /skills/:id`** → full skill object (schema per F-04)
601
+
602
+ **`POST /agent/handle`**
603
+ ```
604
+ Request: {
605
+ "scenario": "Enterprise customer, 18 months tenure, wants $1,200 refund",
606
+ "context": { "plan": "enterprise", "tenure_months": 18, "refund_amount": 1200 },
607
+ "with_brain": true
608
+ }
609
+ ```
610
+
611
+ **`with_brain` flag behaviour (fully specified):**
612
+ - `with_brain: true` → system prompt includes: company name (Rivanly), active brain version, relevant skill in markdown, all decision conditions, forbidden actions, escalation chain
613
+ - `with_brain: false` → system prompt contains ONLY the raw scenario text. No company name. No skills context. No Rivanly-specific information. No hint that a brain exists. The goal is to produce a generic response from the base model that demonstrates what agents do WITHOUT the compilation layer. This is the "before" panel in the side-by-side comparison.
614
+
615
+ **`GET /compile/stream?job_id=uuid`** — SSE event schema:
616
+ ```
617
+ event: node_start
618
+ data: {"node": "ingest_slack", "timestamp": "2026-05-04T09:30:01Z"}
619
+
620
+ event: node_complete
621
+ data: {"node": "ingest_slack", "duration_ms": 312, "output_count": 47}
622
+
623
+ event: node_error
624
+ data: {"node": "extract_decisions", "error": "llm_malformed_json", "retrying": true}
625
+
626
+ event: compile_complete
627
+ data: {
628
+ "brain_version": "1.3.0",
629
+ "total_skills": 12,
630
+ "stale_skills": 0,
631
+ "duration_ms": 54200,
632
+ "skills_below_threshold": 1
633
+ }
634
+
635
+ event: compile_error
636
+ data: {"error": "llm_unavailable", "checkpoint_saved": true, "resume_job_id": "uuid"}
637
+ ```
638
+
639
+ **`GET /diff/:v1/:v2`**
640
+ ```
641
+ Response: {
642
+ "from_version": "1.2.0", "to_version": "1.3.0",
643
+ "added_skills": [], "removed_skills": [],
644
+ "modified_skills": [
645
+ { "id": "handle_refund_request",
646
+ "changes": [{"field": "decision_logic[1].action",
647
+ "from": "approve_prorated_refund", "to": "escalate_to_am"}] }
648
+ ]
649
+ }
650
+ ```
651
+
652
+ **`POST /sources/upload`**
653
+ ```
654
+ Request: multipart/form-data: files[], company_id
655
+ Response: { "uploaded": ["notion_refund_sop.md"], "hashes": {"notion_refund_sop.md": "sha256:a1b2c3..."} }
656
+ ```
657
+
658
+ ---
659
+
660
+ ## 8. AI System Requirements
661
+
662
+ ### 8.1 Tool & Model Requirements
663
+
664
+ | Component | Tool / Model | Reason |
665
+ |---|---|---|
666
+ | All LLM extraction calls | `RedHatAI/Qwen2.5-72B-Instruct-FP8-dynamic` via vLLM | Best instruction following; FP8 = 1.5× throughput, ~72GB VRAM |
667
+ | Skill matching (v0) | `all-MiniLM-L6-v2` via `sentence-transformers` (in-memory, CPU) | Zero infra overhead; sufficient for 12 skills |
668
+ | Skill matching (v1) | pgvector on Supabase | Multi-tenant, persistent, scalable |
669
+ | LLM fallback | `Llama-3.3-70B BF16` | If Qwen2.5 unavailable on MI300X |
670
+ | Serving | vLLM on AMD MI300X (192GB VRAM) | Parallel batch inference, OpenAI-compatible API |
671
+
672
+ ### 8.2 Extraction Prompts — Requirements
673
+
674
+ - All extractors demand: "Output ONLY structured JSON. Do not summarize. Do not generalize beyond what the text explicitly supports."
675
+ - All extractors include: output schema definition + 1-shot example in system prompt
676
+ - Temperature: 0.1 (deterministic extraction)
677
+ - Max tokens: 4096 per call
678
+
679
+ ### 8.3 Evaluation Strategy
680
+
681
+ | Eval | Target | How to test |
682
+ |---|---|---|
683
+ | Brain agent correct action | 12 / 12 (100%) | Run all 12 scenarios against ground truth table |
684
+ | Evidence coverage | 100% of `decision_logic` entries have ≥ 1 `evidence_sources` | JSON schema validation post-compile |
685
+ | Contradiction recall | ≥ 2 contradictions flagged | Plant 2 deliberate contradictions in synthetic dataset |
686
+ | Confidence calibration | Well-sourced skills ≥ 0.7, single-source skills < 0.75 | Inspect post-compile |
687
+ | LLM JSON validity | 0 uncaught malformed responses in 10-run stress test | Run compile 10× on same dataset |
688
+ | "Without brain" failure rate | ≥ 8 of 12 scenarios produce generic/wrong response | Verify demo panel contrast is meaningful |
689
+
690
+ ---
691
+
692
+ ## 9. Implementation Decisions
693
+
694
+ **`with_brain: false` — Full Specification**
695
+
696
+ When `POST /agent/handle` is called with `"with_brain": false`, the system prompt sent to Qwen2.5-72B contains ONLY this:
697
+
698
+ ```
699
+ You are a helpful customer support assistant.
700
+ The customer says: {scenario}
701
+ {context if provided}
702
+ Respond appropriately.
703
+ ```
704
+
705
+ No company name. No skills. No Rivanly. No hint that any compiled knowledge exists. The base model responds from its training data alone. This produces a generic, policy-free response — which is the "before" state that makes the "with brain" response look like magic.
706
+
707
+ **Other key architectural decisions:**
708
+
709
+ - `ingest_join` + `Send` API pattern required for correct LangGraph fan-in. Direct edges ingest→extract cause synthesis to fire before all extractors complete.
710
+ - `Annotated[List, operator.add]` on all extraction output fields required for parallel writes to merge correctly rather than overwrite.
711
+ - `await compiled_graph.ainvoke(initial_state)` — not `.invoke()` — in FastAPI background task. Without async, nodes block the event loop and parallelism is lost.
712
+ - Skills file is the only source of truth. The agent never reads raw source files at query time.
713
+ - `skills_files.is_current` enforced via partial unique index — only one row per company can be `true` at a time.
714
+ - `compile_runs` table is append-only. No updates.
715
+
716
+ ---
717
+
718
+ ## 10. Technical Specifications
719
+
720
+ ### 10.1 Architecture Overview
721
+
722
+ ```
723
+ FILE UPLOAD (Next.js)
724
+
725
+ ▼ POST /sources/upload
726
+ FASTAPI API LAYER
727
+
728
+ ▼ POST /compile → ainvoke
729
+ LANGGRAPH ENGINE (BrainState)
730
+
731
+ ├── INGESTION (parallel, CPU)
732
+ │ ├── ingest_slack → normalized_events[]
733
+ │ ├── ingest_notion → structured_sops[]
734
+ │ └── ingest_tickets → resolved_cases[]
735
+ │ └── ingest_join (barrier)
736
+
737
+ ├── EXTRACTION (parallel, async, AMD MI300X)
738
+ │ ├── extract_decisions → raw_decisions[]
739
+ │ ├── extract_workflows → workflow_steps[]
740
+ │ ├── extract_exceptions → exception_rules[]
741
+ │ └── detect_contradictions → contradictions[]
742
+
743
+ └── COMPILATION + VALIDATION (sequential)
744
+ ├── synthesize_skills → draft_skills[]
745
+ ├── link_evidence → skills_with_evidence[]
746
+ ├── score_confidence → confidence per skill
747
+ └── write_brain → skills_file.json → Supabase
748
+
749
+ BRAIN AGENT (query time)
750
+ POST /agent/handle
751
+ → sentence-transformers match → skill JSON → markdown
752
+ → single vLLM call → structured response JSON
753
+ ```
754
+
755
+ ### 10.2 Screen-to-Screen User Flow
756
+
757
+ **Primary flow (Ops Owner compiling and testing for the first time):**
758
+
759
+ ```
760
+ [Upload Sources page]
761
+ → Upload 3–8 files (drag + drop or file picker)
762
+ → See file list with SHA-256 hash status (new / unchanged / changed)
763
+ → Click "Done — Go to Dashboard"
764
+
765
+ [Brain Dashboard]
766
+ → See: company name, current brain version (or "No brain yet"), last compiled timestamp
767
+ → See: source files uploaded (count)
768
+ → Click "Build Company Brain" button
769
+
770
+ [SSE Progress overlay — renders in-place on Dashboard]
771
+ → Real-time: each node appears as it starts, gets checkmark when complete
772
+ → ingest_slack ✓ → ingest_notion ✓ → ingest_tickets ✓ → [join]
773
+ → extract_decisions ✓ (parallel) extract_workflows ✓ extract_exceptions ✓ detect_contradictions ✓
774
+ → synthesize_skills ✓ → link_evidence ✓ → score_confidence ✓ → write_brain ✓
775
+ → "Brain compiled: v1.3.0 in 58 seconds"
776
+
777
+ [Brain Dashboard — updated state]
778
+ → Version badge updated: v1.3.0
779
+ → Last compiled: just now
780
+ → 12 skills / 6 departments / 0 stale
781
+ → Click "View Skills" (or nav to Skills in sidebar)
782
+
783
+ [Skills Viewer]
784
+ → 6 department groups, 12 skill cards
785
+ → Each card: name, confidence bar, stale badge (if applicable)
786
+ → Click any skill card → detail panel expands right
787
+ → Detail: all conditions, forbidden actions, escalation chain, evidence panel (source + excerpt)
788
+ → Click "Try a scenario" button (appears in detail panel)
789
+
790
+ [Demo Agent Panel]
791
+ → Left panel: "Without Brain" — base model response (generic)
792
+ → Right panel: "With Brain" — rule trace + evidence + action
793
+ → Scenario input pre-filled from skill that was clicked (optional convenience)
794
+ → Submit → both panels render simultaneously
795
+ → Judge reads both — value is self-evident
796
+ → Click "What changed?" link (appears in top nav after ≥ 2 brain versions exist)
797
+
798
+ [Version Diff View]
799
+ → Select v1 and v2 from dropdowns (pre-seeded with v1.1.0 and v1.2.0)
800
+ → See: modified skills (yellow), new skills (green), removed (red)
801
+ → Click modified skill → see field-level diff of changed conditions
802
+ → Click "← Back to Dashboard" (always accessible from nav)
803
+
804
+ [Brain Dashboard]
805
+ → Modify a source file → re-upload → stale badge appears on affected skills
806
+ → Click "Build Company Brain" → recompile cycle repeats
807
+ ```
808
+
809
+ **Critical path for demo (8-step script):**
810
+ Upload Sources → Dashboard → Build → SSE feed → Skills Viewer → Evidence panel → Demo Agent (side-by-side) → Change + Rebuild → Diff view
811
+
812
+ **Navigation rules:**
813
+ - Sidebar always visible: Dashboard | Skills | Agent | Diff
814
+ - "Try a scenario" shortcut from Skills Viewer pre-fills the Agent panel's skill context
815
+ - "What changed?" link only appears when ≥ 2 brain versions exist (prevents confusion when first compiled)
816
+ - All pages accessible from nav at any time — no forced linear flow outside the demo script
817
+
818
+ ### 10.3 Integration Points
819
+
820
+ | Integration | v0 | v1 |
821
+ |---|---|---|
822
+ | LLM | vLLM on AMD MI300X (private IP:8000) | Same + failover |
823
+ | Database | Supabase Postgres | Same + RLS per company |
824
+ | File storage | Supabase Storage | Same |
825
+ | Auth | None | Clerk |
826
+ | Queue | None (direct ainvoke) | Redis/Upstash |
827
+ | Connectors | File upload only | Slack OAuth, Notion API, Zendesk |
828
+ | Checkpointing | MemorySaver (in-memory) | PostgresSaver |
829
+
830
+ ### 10.4 Security & Privacy
831
+
832
+ **v0 (hackathon):** All data is synthetic (Rivanly is fictional). No PII. vLLM on private AMD cloud IP. No RLS needed.
833
+
834
+ **v1 (required before real customer data):**
835
+ - Clerk auth on all endpoints
836
+ - Supabase RLS: `company_id` row-level isolation
837
+ - vLLM behind VPC — not publicly accessible
838
+ - No customer message content stored permanently — only extracted rules and evidence excerpts
839
+
840
+ ---
841
+
842
+ ## 11. Data Model (Supabase)
843
+
844
+ ```sql
845
+ CREATE TABLE companies (
846
+ id TEXT PRIMARY KEY,
847
+ name TEXT NOT NULL,
848
+ created_at TIMESTAMPTZ DEFAULT now()
849
+ );
850
+
851
+ CREATE TABLE skills_files (
852
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
853
+ company_id TEXT REFERENCES companies(id),
854
+ version TEXT NOT NULL,
855
+ brain_json JSONB NOT NULL,
856
+ source_hashes JSONB NOT NULL,
857
+ compiled_at TIMESTAMPTZ DEFAULT now(),
858
+ is_current BOOLEAN DEFAULT false
859
+ );
860
+ CREATE UNIQUE INDEX idx_skills_files_current ON skills_files(company_id) WHERE is_current = true;
861
+
862
+ CREATE TABLE skills (
863
+ id TEXT NOT NULL,
864
+ company_id TEXT REFERENCES companies(id),
865
+ skills_file_id UUID REFERENCES skills_files(id),
866
+ name TEXT NOT NULL,
867
+ domain TEXT NOT NULL,
868
+ version TEXT NOT NULL,
869
+ confidence FLOAT NOT NULL,
870
+ stale BOOLEAN DEFAULT false,
871
+ review_required BOOLEAN DEFAULT false,
872
+ skill_json JSONB NOT NULL,
873
+ PRIMARY KEY (id, company_id, skills_file_id)
874
+ );
875
+
876
+ CREATE TABLE source_files (
877
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
878
+ company_id TEXT REFERENCES companies(id),
879
+ filename TEXT NOT NULL,
880
+ sha256 TEXT NOT NULL,
881
+ storage_path TEXT NOT NULL,
882
+ uploaded_at TIMESTAMPTZ DEFAULT now()
883
+ );
884
+
885
+ CREATE TABLE compile_runs (
886
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
887
+ company_id TEXT REFERENCES companies(id),
888
+ status TEXT NOT NULL CHECK (status IN ('started','running','complete','error')),
889
+ started_at TIMESTAMPTZ DEFAULT now(),
890
+ completed_at TIMESTAMPTZ,
891
+ duration_ms INTEGER,
892
+ result_version TEXT,
893
+ error_detail TEXT
894
+ );
895
+
896
+ CREATE INDEX idx_skills_files_company ON skills_files(company_id, compiled_at DESC);
897
+ CREATE INDEX idx_skills_company ON skills(company_id);
898
+ ```
899
+
900
+ ---
901
+
902
+ ## 12. Testing Decisions
903
+
904
+ ### Ground Truth Test Suite — All 12 Scenarios (COMPLETE)
905
+
906
+ **Owner: Abhijith. Must be run and passing before demo day. All 12 must return correct `action`.**
907
+
908
+ | # | Scenario | Key Context | Expected `action` | Expected `skill_matched` |
909
+ |---|---|---|---|---|
910
+ | 1 | Enterprise customer, 18 months tenure, $1,200 refund requested | plan=enterprise, tenure=18mo, amount=1200 | `escalate_to_am_within_1hr` | `handle_refund_request` |
911
+ | 2 | Annual plan customer, day 10 of subscription, $300 refund requested | plan=annual, days_since_purchase=10, amount=300 | `approve_full_refund` | `handle_refund_request` |
912
+ | 3 | New customer, 2 months tenure, $600 refund requested | plan=monthly, tenure=2mo, amount=600 | `escalate_to_founder` | `handle_refund_request` |
913
+ | 4 | Loyal annual customer, 14 months tenure, $150 refund outside window | plan=annual, tenure=14mo, amount=150 | `approve_prorated_refund` | `handle_refund_request` |
914
+ | 5 | Lifetime deal customer requesting any refund | plan=lifetime, amount=any | `deny_refund_ltd_terms` | `handle_refund_request` |
915
+ | 6 | Customer contact during active platform outage | context=outage_active | `send_incident_response_template` | `respond_to_outage` |
916
+ | 7 | Startup customer requesting 40% discount | customer_type=startup, discount_requested=40% | `escalate_to_ae` | `evaluate_discount_request` |
917
+ | 8 | P0 bug reported on dashboard module by enterprise customer | bug_severity=P0, customer_plan=enterprise | `page_oncall_engineer_immediately` | `prioritize_bug_report` |
918
+ | 9 | Customer SLA breached by 2 hours, enterprise plan | sla_breach_hours=2, plan=enterprise | `notify_am_and_eng_lead` | `handle_sla_breach` |
919
+ | 10 | Customer showing 3 churn signals in last 30 days (no login, support ticket, downgrade inquiry) | signals=3, timeframe=30days | `schedule_am_call_within_24h` | `evaluate_churn_risk` |
920
+ | 11 | Engineering candidate — completed 2 rounds, needs offer approval | stage=offer, role=engineer | `get_founder_approval_before_sending` | `hiring_process_engineering` |
921
+ | 12 | Vendor invoice for $3,500 needs payment approval | amount=3500, vendor_type=software | `route_to_ops_lead_approval` | `approve_vendor_payment` |
922
+
923
+ **How to run:**
924
+ ```python
925
+ # Run before demo. All 12 must pass.
926
+ for scenario in GROUND_TRUTH_SCENARIOS:
927
+ response = client.post("/agent/handle", json=scenario["input"])
928
+ assert response.json()["action"] == scenario["expected_action"]
929
+ assert response.json()["skill_matched"] == scenario["expected_skill"]
930
+ ```
931
+
932
+ ### Module Test Matrix
933
+
934
+ | Module | Test Type | What to Test |
935
+ |---|---|---|
936
+ | Source parsers | Unit | Given raw fixture file → correct normalized output shape |
937
+ | SHA-256 hasher | Unit | Same content → same hash; changed content → different hash |
938
+ | Skill matcher | Unit | Given 12 known queries → each returns correct `skill_id` |
939
+ | JSON→Markdown converter | Unit | Given skill object → output contains all conditions and forbidden actions, under 800 tokens |
940
+ | `POST /compile` | Integration | Returns `job_id` and `stream_url`; sets compile_run status to "started" |
941
+ | `GET /skills` | Integration | Returns exactly 12 skills for Rivanly |
942
+ | `POST /agent/handle` | Integration | All 12 ground-truth scenarios return correct `action` |
943
+ | `GET /diff/:v1/:v2` | Integration | Pre-seeded v1.1.0 and v1.2.0 → returns expected `modified_skills` |
944
+ | Full pipeline | End-to-end | 8 source files → 12 skills in Supabase, `is_current: true`, all with evidence |
945
+ | LLM output | Eval | 10-run stress test → zero uncaught malformed JSON |
946
+
947
+ ---
948
+
949
+ ## 13. Non-Functional Requirements
950
+
951
+ - Full compilation: under 90 seconds (target: 60s)
952
+ - Brain agent response: under 8 seconds
953
+ - SSE feed: real-time node events, no polling
954
+ - Skill matching: under 200ms (in-memory cosine similarity)
955
+ - LangGraph MemorySaver checkpointing: compile state survives crash
956
+ - Fallback model: Llama-3.3-70B BF16 if Qwen2.5 unavailable
957
+ - vLLM health check queried before accepting `/compile` requests
958
+
959
+ ---
960
+
961
+ ## 14. Success Metrics
962
+
963
+ ### Hackathon v0 — Measurable Targets
964
+
965
+ | Metric | Target | Verification |
966
+ |---|---|---|
967
+ | End-to-end pipeline | Completes without error | Run 3× in final 2 hours |
968
+ | Skills produced | Exactly 12 | Check `skills_file.json` |
969
+ | Skills with confidence ≥ 0.7 | ≥ 10 of 12 | Check confidence field |
970
+ | Agent correct action | 12 / 12 | Run ground truth suite |
971
+ | Agent latency | < 8 seconds | Time on demo day |
972
+ | Compilation time | < 90 seconds | Dashboard display |
973
+ | Live URL accessible | Yes | Test on fresh device before submission |
974
+ | Demo video submitted | Yes | Render early, keep backup |
975
+ | Public posts | 2 minimum | During hours 8–16 and 16–28 |
976
+
977
+ ### The 8-Step Demo — Ring 1 Acceptance Test
978
+
979
+ 1. Show source files — "Rivanly's scattered knowledge."
980
+ 2. Click "Build Company Brain" — watch SSE feed in real time.
981
+ 3. Show compilation time — "12 skills in 58 seconds on AMD MI300X."
982
+ 4. Open Skills Viewer — 6 departments, 12 skills, confidence bars.
983
+ 5. Click `handle_refund_request` — show evidence panel.
984
+ 6. Submit enterprise refund scenario to agent panel.
985
+ 7. Show side-by-side: without brain (generic) vs. with brain (rule trace + evidence).
986
+ 8. Change one SOP rule → Rebuild → same scenario → different outcome. **This is the moment.**
987
+
988
+ ### Post-Hackathon Business Metrics (v1)
989
+
990
+ - 3 paying pilot customers within 60 days of v1 launch
991
+ - Activation: first brain compiled + agent handles 1 scenario correctly
992
+ - Retention: brain recompiled at least once within 30 days
993
+ - Revenue: $200/month Starter, $500/month Growth
994
+
995
+ ---
996
+
997
+ ## 15. Competitive Landscape
998
+
999
+ *Updated with companies identified in YC/LinkedIn Company Brain thread.*
1000
+
1001
+ | Company | What they do | Differentiation |
1002
+ |---|---|---|
1003
+ | **Notion AI** | Q&A over documents | Retrieves chunks, doesn't compile operational judgment |
1004
+ | **Guru / Confluence** | Knowledge base search | Human-maintained, not executable by AI agents |
1005
+ | **Glean** | Enterprise search | Search-first, not compilation; no executable output |
1006
+ | **Sugarwork** (sugarwork.com) | Surfaces tacit knowledge for AI | Adjacent; watch closely |
1007
+ | **BrandOS** (getbrandos.site) | Company brain for marketing teams | Vertical-specific; not full company coverage |
1008
+ | **Context AI** | Operational knowledge for agents | Direct competitor — monitor |
1009
+ | **LineageOne** (NEXT'26) | Fragmented operations → live operational model | Direct competitor |
1010
+ | **AutoBase** | Building this for 7 months | Direct competitor |
1011
+ | **Company Brain** | Full compilation layer, all departments, versioned, evidence-linked | Evidence trail, stale detection, parallel AMD compilation |
1012
+
1013
+ **Observation:** Multiple teams are building in this space. This validates the market. The race is to who ships the most complete, demo-able, production-credible version. Company Brain's differentiator is the combination of: evidence-linked rules (not just structured outputs), stale detection, version diffing, and the clean "compiler not assistant" framing that competitors haven't articulated.
1014
+
1015
+ ---
1016
+
1017
+ ## 16. Risks & Mitigation
1018
+
1019
+ | Risk | Likelihood | Mitigation |
1020
+ |---|---|---|
1021
+ | Knowledge that was never captured cannot be extracted | **High — acknowledged** | Scope v0 to knowledge that exists in digital form; call out in pitch as known limitation; v1 adds call transcription |
1022
+ | Extraction agents produce low-quality skills | Medium | Dataset authored backward from desired output; eval suite catches failures before demo |
1023
+ | vLLM setup on AMD cloud takes too long | Low | Kubernetes on AMD course completed; fallback to Fireworks API |
1024
+ | LangGraph parallel fan-in bug | Low | Fixed using `Send` API + `ingest_join` barrier node |
1025
+ | Demo breaks during judging | Medium | Pre-recorded fallback video; deploy to stable URL 24h before submission |
1026
+ | Qwen2.5-72B FP8 unavailable | Low | `RedHatAI/Qwen2.5-72B-Instruct-FP8-dynamic` confirmed on HuggingFace |
1027
+ | Frontend/backend API contract mismatch | Medium | Both parties agree on F-12 schemas before writing frontend code |
1028
+ | Synthetic dataset too shallow | Medium | Each file: ≥ 4 edge cases, ≥ 1 planted contradiction; reviewed together before kickoff |
1029
+ | Competitors ship demo before May 11 | Low | Multiple are building but none have shipped a demo yet; Company Brain's AMD + parallel compile angle is unique |
1030
+
1031
+ ---
1032
+
1033
+ ## 17. v2 Roadmap — Insights from LinkedIn Thread
1034
+
1035
+ *Insights from practitioners who responded to Tom Blomfield's YC RFS post that should inform v2 product decisions.*
1036
+
1037
+ **Execution boundaries (Horizon Labs insight):** The skills file is currently advisory — the agent reads it and acts. In v2, the skills file should become constraining — the agent should not be able to take actions not in the admissible action set. This is the difference between a knowledge map and an execution boundary. Add to v2: `forbidden_actions` enforced at the runtime level, not just injected as prompt guidance.
1038
+
1039
+ **The stale knowledge divergence problem (Matan Elmalam insight):** Teams build the map once, ship the agent, and within six weeks reality diverges. Our stale detection addresses this for captured knowledge. For v2: active monitoring — compare agent actions against skills file weekly and surface divergences as "possible new skills" for human review.
1040
+
1041
+ **Call transcription (Paul Breuler gap):** Knowledge that exists only in spoken conversations will never be in Slack or Notion. In v2: integrate with Fireflies/Otter/Grain to pull meeting transcripts as a first-class source type. This closes the most common knowledge capture gap.
1042
+
1043
+ **Audit trail (Josh Jefferd insight):** Every agent action should be logged with which skill rule was applied and which evidence excerpt justified it. This is the compliance and trust layer. Add to v2 roadmap as a first-class feature, not an afterthought.
1044
+
1045
+ ---
1046
+
1047
+ ## 18. Open Questions — All Resolved
1048
+
1049
+ | Question | Resolution |
1050
+ |---|---|
1051
+ | Who owns frontend vs. pipeline? | Abhijith = pipeline + API. Harshit = all frontend. |
1052
+ | Supabase schema? | Defined in Section 11. |
1053
+ | SSE disconnect/reconnect handling? | Frontend: exponential backoff (1s, 2s, 4s). Fallback: `GET /brain/status` for final state. |
1054
+ | Synthetic dataset ownership? | Both — 4 files each, authored before May 4 kickoff. |
1055
+ | Ground truth table complete? | Yes — all 12 scenarios in Section 12. Run before demo. |
1056
+ | `with_brain: false` behaviour? | Fully specified in F-10 and Section 9. |
1057
+ | Screen-to-screen user flow? | Defined in Section 10.2. |
1058
+
1059
+ ---
1060
+
1061
+ *This document supersedes company_brain_PRD_v3.md. All three audit issues resolved. Competitive landscape updated with real companies from LinkedIn thread. No scope changes after May 4 kickoff.*
data/sources/rivanly-inc/notion_cs_playbook.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Customer Success Playbook
2
+ **Department:** Customer Success
3
+ **Last Updated:** May 2026
4
+
5
+ ## 1. Churn Risk Evaluation
6
+ It is critical to identify and intervene when accounts show signs of churning.
7
+ - **Rule:** If a customer exhibits 3 or more churn signals (e.g., no logins, support ticket escalations, downgrade inquiries) within a 30-day timeframe, you must schedule an AM call within 24 hours.
8
+
9
+ ## 2. Enterprise Onboarding
10
+ - **Rule:** For all new Enterprise customers, the onboarding process must include a dedicated kickoff call, a customized training session, and a 30-day check-in.
data/sources/rivanly-inc/notion_eng_runbook.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Engineering Runbook & SLAs
2
+ **Department:** Product & Engineering
3
+ **Last Updated:** February 2026
4
+
5
+ ## 1. Bug Triage
6
+ - **P0 (Critical):** System down, data loss, or core workflow broken.
7
+ - **Rule:** If a P0 bug is reported by an Enterprise customer, page the on-call engineer immediately.
8
+ - **P1 (High):** Major feature broken but workaround exists.
9
+ - **Rule:** P1 bugs must be resolved within 4 hours.
10
+ - **P2 (Medium/Low):** UI glitches, minor inconveniences. Add to the backlog.
11
+
12
+ ## 2. SLA Breach Handling
13
+ - **Standard Process:** If a customer SLA is breached by more than 1 hour, notify the support lead.
14
+ - **Enterprise Exceptions:** If an Enterprise plan customer SLA is breached by 2 hours or more, you must notify both the Account Manager and the Engineering Lead immediately.
15
+
16
+ ## 3. Outage Response
17
+ - If a customer contacts support during an active platform outage, do not troubleshoot. Send the standard incident response template and link to the status page.
data/sources/rivanly-inc/notion_hr_playbook.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HR & Hiring Playbook
2
+ **Department:** HR
3
+ **Last Updated:** January 2026
4
+
5
+ ## 1. Engineering Hiring Process
6
+ The standard hiring process for Engineering roles:
7
+ 1. Recruiter Screen
8
+ 2. Technical Interview (Pair Programming)
9
+ 3. Systems Design Interview
10
+ 4. Culture Fit with Founders
11
+ 5. Offer Stage
12
+
13
+ **Critical Rule:** For any engineering candidate at the offer stage, you must get Founder approval before sending the final offer letter.
14
+
15
+ ## 2. Performance & PIPs
16
+ - A Performance Improvement Plan (PIP) is triggered if an employee misses their core KPIs for two consecutive quarters.
17
+ - If an employee is placed on a PIP, HR must schedule a formal review with the department head within 5 business days.
data/sources/rivanly-inc/notion_pricing_policy.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pricing & Discount Policy
2
+ **Department:** Revenue
3
+ **Last Updated:** April 2026
4
+
5
+ ## 1. Overview
6
+ This document outlines standard pricing exceptions and discount approval chains.
7
+
8
+ ## 2. Discount Authority
9
+ - **Standard Discount:** Support and CS can apply up to a 10% discount to save a churning customer.
10
+ - **Startup Discount:** If a customer identifies as an early-stage startup (pre-seed or seed), you may approve up to a 20% discount on the Annual plan for the first year.
11
+ - **Large Discounts:** If a customer requests a discount greater than 30%, it must be escalated to an Account Executive (AE) for approval. Support cannot approve this.
12
+
13
+ ## 3. Custom Pricing
14
+ - **Enterprise Custom Pricing:** Enterprise customers requesting custom feature bundles or volume-based pricing must be routed to the VP of Sales.
data/sources/rivanly-inc/notion_refund_sop.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Refund Standard Operating Procedure (SOP)
2
+ **Department:** Support
3
+ **Last Updated:** March 2026
4
+
5
+ ## 1. Core Policy
6
+ Our refund policy is designed to balance customer satisfaction with revenue retention. Always aim to understand the root cause before processing a refund.
7
+
8
+ ## 2. Refund Eligibility & Rules
9
+ - **Annual Plans (First 14 days):** If a customer on an annual plan requests a refund within the first 14 days of purchase, approve a full refund immediately. No questions asked.
10
+ - **Annual Plans (After 14 days):** If a customer on an annual plan requests a refund after 14 days, approve a prorated refund for the remaining unused months.
11
+ - **Enterprise Customers:** If any Enterprise customer requests a refund of any amount, DO NOT process it immediately. You must escalate to the Account Manager (AM) within 1 hour.
12
+ - **Lifetime Deals (LTD):** Under no circumstances do we process refunds for lifetime deal accounts. Deny the request citing LTD terms.
13
+ - **Monthly Plans (New Customers):** If a customer on a monthly plan with a tenure of less than 3 months requests a refund over $500, escalate to the Founder.
14
+
15
+ ## 3. Strict Time Limits
16
+ **CRITICAL:** We offer absolutely no refunds after 30 days of purchase for any customer tier. If the purchase was more than 30 days ago, deny the refund.
data/sources/rivanly-inc/slack_export_ops.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "channel": "finance-ops",
4
+ "user": "jessica_fin",
5
+ "text": "I have an incoming vendor invoice for $3,500 from Datadog (software vendor). Who needs to approve this?",
6
+ "timestamp": "2026-02-15T09:30:00Z"
7
+ },
8
+ {
9
+ "channel": "finance-ops",
10
+ "user": "david_ops_lead",
11
+ "text": "Any software vendor invoice of $3,500 or more needs to be routed to the ops lead for approval before finance pays it. Send it my way.",
12
+ "timestamp": "2026-02-15T09:45:00Z"
13
+ },
14
+ {
15
+ "channel": "finance-ops",
16
+ "user": "jessica_fin",
17
+ "text": "Got it, routing it to you.",
18
+ "timestamp": "2026-02-15T09:46:00Z"
19
+ }
20
+ ]
data/sources/rivanly-inc/slack_export_support.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "channel": "support-triage",
4
+ "user": "sarah_am",
5
+ "text": "Hey team, Acme Corp (been with us 4 years) is asking for a refund for last month's invoice due to the billing mixup. It's been 45 days since that charge, I know SOP says 30 days max.",
6
+ "timestamp": "2026-03-12T10:00:00Z"
7
+ },
8
+ {
9
+ "channel": "support-triage",
10
+ "user": "mike_lead",
11
+ "text": "For loyal customers over 2 years tenure, we can bypass the 30-day rule. Go ahead and approve the refund for Acme Corp.",
12
+ "timestamp": "2026-03-12T10:05:00Z"
13
+ },
14
+ {
15
+ "channel": "support-triage",
16
+ "user": "alex_support",
17
+ "text": "We have an active platform outage affecting all EU servers. Customers are opening tickets left and right.",
18
+ "timestamp": "2026-04-01T14:20:00Z"
19
+ },
20
+ {
21
+ "channel": "support-triage",
22
+ "user": "mike_lead",
23
+ "text": "Do not try to troubleshoot individual EU tickets right now. Just send the incident response template and close the tickets.",
24
+ "timestamp": "2026-04-01T14:22:00Z"
25
+ }
26
+ ]
data/sources/rivanly-inc/zendesk_tickets.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "TICKET-1042",
4
+ "subject": "Dashboard not loading",
5
+ "description": "I cannot access the main dashboard. It just spins forever.",
6
+ "resolution": "P0 bug confirmed. Paged on-call engineer immediately as this is an Enterprise customer.",
7
+ "tags": ["bug", "P0", "enterprise"]
8
+ },
9
+ {
10
+ "id": "TICKET-1045",
11
+ "subject": "Export feature failing",
12
+ "description": "When I try to export reports to CSV, it fails.",
13
+ "resolution": "Confirmed P1 bug for Enterprise customer GlobalCorp. Escalated to Eng and resolved same-day (within 12 hours), outside the normal 4-hour SLA but acceptable for this specific complex issue for Enterprise.",
14
+ "tags": ["bug", "P1", "enterprise"]
15
+ },
16
+ {
17
+ "id": "TICKET-1088",
18
+ "subject": "SLA breached on ticket 1087",
19
+ "description": "We have been waiting 6 hours for a response.",
20
+ "resolution": "SLA breached by 2 hours for Enterprise customer. Notified AM and Eng Lead immediately.",
21
+ "tags": ["sla_breach", "enterprise"]
22
+ }
23
+ ]
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
frontend/AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
frontend/CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
frontend/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
frontend/next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "next": "16.2.5",
13
+ "react": "19.2.4",
14
+ "react-dom": "19.2.4"
15
+ },
16
+ "devDependencies": {
17
+ "@tailwindcss/postcss": "^4",
18
+ "@types/node": "^20",
19
+ "@types/react": "^19",
20
+ "@types/react-dom": "^19",
21
+ "eslint": "^9",
22
+ "eslint-config-next": "16.2.5",
23
+ "tailwindcss": "^4",
24
+ "typescript": "^5"
25
+ }
26
+ }
frontend/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
frontend/public/file.svg ADDED
frontend/public/globe.svg ADDED
frontend/public/next.svg ADDED
frontend/public/vercel.svg ADDED
frontend/public/window.svg ADDED
frontend/src/app/compile/[jobId]/page.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState, use } from "react";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ interface LogEvent {
7
+ timestamp: string;
8
+ type: string;
9
+ data: any;
10
+ }
11
+
12
+ const STAGE_LABELS: Record<string, string> = {
13
+ pipeline_start: "🚀 Pipeline Started",
14
+ LOADING_DOCS: "📂 Loading Documents",
15
+ CHUNKING: "✂️ Chunking Documents",
16
+ CHUNKING_DONE: "✅ Chunking Complete",
17
+ EMBEDDING: "🧠 Embedding & Clustering",
18
+ EMBEDDING_DONE: "✅ Clustering Complete",
19
+ SYNTHESIZING_SKILLS: "⚡ Synthesizing Skills",
20
+ QUALITY_CHECK: "🔍 Quality & Confidence Scoring",
21
+ QUALITY_CHECK_DONE: "✅ Quality Check Complete",
22
+ WRITING_DB: "💾 Writing to Database",
23
+ DONE: "✅ Pipeline Complete",
24
+ pipeline_complete: "🎉 Compilation Finished",
25
+ pipeline_error: "❌ Pipeline Error",
26
+ };
27
+
28
+ export default function CompileViewer({ params }: { params: Promise<{ jobId: string }> }) {
29
+ const resolvedParams = use(params);
30
+ const jobId = resolvedParams.jobId;
31
+ const [logs, setLogs] = useState<LogEvent[]>([]);
32
+ const [status, setStatus] = useState("Connecting...");
33
+ const router = useRouter();
34
+
35
+ useEffect(() => {
36
+ if (!jobId) return;
37
+
38
+ const eventSource = new EventSource(`http://localhost:8080/compile/${jobId}/stream`);
39
+
40
+ eventSource.onmessage = (event) => {
41
+ const parsed = JSON.parse(event.data);
42
+ const eventType = parsed.event;
43
+ const eventData = parsed.data;
44
+
45
+ setLogs((prev) => [
46
+ ...prev,
47
+ { timestamp: new Date().toLocaleTimeString(), type: eventType, data: eventData },
48
+ ]);
49
+
50
+ // Update the status bar based on event type
51
+ if (eventType === "stage") {
52
+ const stageName = eventData.name || "";
53
+ const label = STAGE_LABELS[stageName] || stageName;
54
+ const detail = eventData.detail || "";
55
+ setStatus(`${label}${detail ? ` — ${detail}` : ""}`);
56
+ } else if (eventType === "pipeline_start") {
57
+ setStatus(STAGE_LABELS.pipeline_start);
58
+ } else if (eventType === "pipeline_complete") {
59
+ setStatus(STAGE_LABELS.pipeline_complete);
60
+ eventSource.close();
61
+ } else if (eventType === "pipeline_error") {
62
+ setStatus(`❌ Error: ${eventData.error || "Unknown"}`);
63
+ eventSource.close();
64
+ }
65
+ };
66
+
67
+ eventSource.onerror = () => {
68
+ eventSource.close();
69
+ };
70
+
71
+ return () => eventSource.close();
72
+ }, [jobId]);
73
+
74
+ return (
75
+ <div className="min-h-screen p-8 flex flex-col">
76
+ <div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
77
+ <h1 className="text-2xl font-bold text-primary">Pipeline Stream</h1>
78
+ <div className="flex items-center gap-4">
79
+ <span
80
+ className={`px-3 py-1 font-mono text-sm border ${
81
+ status.includes("Finished") || status.includes("Complete")
82
+ ? "border-green-500 text-green-500"
83
+ : status.includes("Error")
84
+ ? "border-red-500 text-red-500"
85
+ : "border-primary text-primary animate-pulse"
86
+ }`}
87
+ >
88
+ {status}
89
+ </span>
90
+ <button onClick={() => router.push("/")} className="text-text-secondary hover:text-foreground">
91
+ Back
92
+ </button>
93
+ </div>
94
+ </div>
95
+
96
+ <div className="flex-1 bg-surface border border-gray-800 p-4 font-mono text-sm overflow-y-auto">
97
+ {logs.map((log, i) => {
98
+ const isStage = log.type === "stage";
99
+ const stageName = isStage ? log.data?.name : log.type;
100
+ const label = STAGE_LABELS[stageName] || stageName;
101
+ const detail = isStage ? log.data?.detail || "" : JSON.stringify(log.data);
102
+ const isError = stageName?.includes("error") || stageName?.includes("Error");
103
+
104
+ return (
105
+ <div key={i} className="mb-2">
106
+ <span className="text-text-secondary">[{log.timestamp}]</span>{" "}
107
+ <span className={isError ? "text-red-500" : "text-primary"}>{label}</span>{" "}
108
+ <span className="text-foreground">{detail}</span>
109
+ </div>
110
+ );
111
+ })}
112
+ </div>
113
+ </div>
114
+ );
115
+ }
frontend/src/app/demo/[companyId]/page.tsx ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, use } from "react";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ type AgentResponse = {
7
+ recommended_action?: string;
8
+ rule_applied?: string;
9
+ evidence?: string[];
10
+ skill_matched?: string;
11
+ confidence?: number;
12
+ retrieval_scores?: number[];
13
+ reasoning?: string;
14
+ error?: string;
15
+ };
16
+
17
+ export default function QueryDemo({ params }: { params: Promise<{ companyId: string }> }) {
18
+ const resolvedParams = use(params);
19
+ const companyId = resolvedParams.companyId;
20
+ const [scenario, setScenario] = useState("");
21
+ const [contextJson, setContextJson] = useState("{}");
22
+ const [loading, setLoading] = useState(false);
23
+
24
+ const [withBrainResponse, setWithBrainResponse] = useState<AgentResponse | null>(null);
25
+ const [withoutBrainResponse, setWithoutBrainResponse] = useState<AgentResponse | null>(null);
26
+
27
+ const router = useRouter();
28
+
29
+ const handleQuery = async (e: React.FormEvent) => {
30
+ e.preventDefault();
31
+ if (!scenario) return;
32
+ setLoading(true);
33
+ setWithBrainResponse(null);
34
+ setWithoutBrainResponse(null);
35
+
36
+ let parsedContext = {};
37
+ try {
38
+ if (contextJson.trim()) {
39
+ parsedContext = JSON.parse(contextJson);
40
+ }
41
+ } catch {
42
+ alert("Invalid JSON in context field");
43
+ setLoading(false);
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const [resWithBrain, resWithoutBrain] = await Promise.all([
49
+ fetch("http://localhost:8080/agent/handle", {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ company_id: companyId, scenario, context: parsedContext, with_brain: true }),
53
+ }),
54
+ fetch("http://localhost:8080/agent/handle", {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({ company_id: companyId, scenario, context: parsedContext, with_brain: false }),
58
+ }),
59
+ ]);
60
+
61
+ setWithBrainResponse(await resWithBrain.json());
62
+ setWithoutBrainResponse(await resWithoutBrain.json());
63
+ } catch (err) {
64
+ console.error(err);
65
+ alert("Query failed — is the backend running?");
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ };
70
+
71
+ const confidenceColor = (c: number) => {
72
+ if (c >= 0.75) return "bg-green-500";
73
+ if (c >= 0.5) return "bg-yellow-500";
74
+ if (c >= 0.25) return "bg-orange-500";
75
+ return "bg-red-500";
76
+ };
77
+
78
+ return (
79
+ <div className="min-h-screen p-8 flex flex-col items-center">
80
+ <div className="w-full max-w-5xl">
81
+ <div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
82
+ <h1 className="text-2xl font-bold text-primary">Brain Query Demo</h1>
83
+ <button onClick={() => router.push("/")} className="text-text-secondary hover:text-foreground">
84
+ Back to Dashboard
85
+ </button>
86
+ </div>
87
+
88
+ <form onSubmit={handleQuery} className="mb-8 bg-surface p-6 border border-gray-800">
89
+ <div className="flex flex-col gap-4">
90
+ <div>
91
+ <label className="block text-text-secondary text-sm font-bold mb-2">Scenario</label>
92
+ <textarea
93
+ className="w-full px-4 py-3 bg-background border border-gray-700 text-foreground focus:outline-none focus:border-primary min-h-[100px]"
94
+ placeholder="Enterprise customer, 18 months tenure, wants $1,200 refund"
95
+ value={scenario}
96
+ onChange={(e) => setScenario(e.target.value)}
97
+ />
98
+ </div>
99
+ <div>
100
+ <label className="block text-text-secondary text-sm font-bold mb-2">Context (JSON)</label>
101
+ <textarea
102
+ className="w-full px-4 py-3 bg-background border border-gray-700 text-foreground focus:outline-none focus:border-primary font-mono text-sm min-h-[80px]"
103
+ placeholder='{"plan": "enterprise", "tenure_months": 18, "refund_amount": 1200}'
104
+ value={contextJson}
105
+ onChange={(e) => setContextJson(e.target.value)}
106
+ />
107
+ </div>
108
+ <button
109
+ type="submit"
110
+ disabled={loading || !scenario}
111
+ className="bg-primary text-background font-bold py-3 px-6 hover:opacity-90 disabled:opacity-50 self-end"
112
+ >
113
+ {loading ? "Thinking..." : "Compare Models"}
114
+ </button>
115
+ </div>
116
+ </form>
117
+
118
+ {(withBrainResponse || withoutBrainResponse) && (
119
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
120
+ {/* WITHOUT BRAIN */}
121
+ <div className="bg-surface border border-gray-800 p-6 opacity-75">
122
+ <h2 className="text-xl font-bold text-gray-400 mb-4 flex items-center gap-2">
123
+ <span className="w-2 h-2 rounded-full bg-gray-500"></span>
124
+ Without Brain (Generic AI)
125
+ </h2>
126
+
127
+ {withoutBrainResponse ? (
128
+ <div className="space-y-4 text-gray-300">
129
+ <div>
130
+ <h3 className="text-gray-500 text-sm font-bold uppercase tracking-wider mb-1">Response</h3>
131
+ <p className="text-lg bg-background p-4 border border-gray-800 rounded">
132
+ {withoutBrainResponse.recommended_action || "No action"}
133
+ </p>
134
+ </div>
135
+ <div>
136
+ <h3 className="text-gray-500 text-sm font-bold uppercase tracking-wider mb-1">Rule Applied</h3>
137
+ <p className="italic">{withoutBrainResponse.rule_applied || "General knowledge"}</p>
138
+ </div>
139
+ {withoutBrainResponse.reasoning && (
140
+ <div>
141
+ <h3 className="text-gray-500 text-sm font-bold uppercase tracking-wider mb-1">Reasoning</h3>
142
+ <p className="text-sm">{withoutBrainResponse.reasoning}</p>
143
+ </div>
144
+ )}
145
+ </div>
146
+ ) : (
147
+ <p>Loading...</p>
148
+ )}
149
+ </div>
150
+
151
+ {/* WITH BRAIN */}
152
+ <div className="bg-surface border-2 border-primary p-6 relative shadow-[0_0_15px_rgba(45,212,191,0.1)]">
153
+ <div className="absolute -top-3 -right-3 bg-primary text-background text-xs font-bold px-3 py-1 uppercase tracking-wider rounded-full">
154
+ Company Brain
155
+ </div>
156
+ <h2 className="text-xl font-bold text-primary mb-4 flex items-center gap-2">
157
+ <span className="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
158
+ With Brain (Compiled Agent)
159
+ </h2>
160
+
161
+ {withBrainResponse ? (
162
+ <div className="space-y-4">
163
+ {withBrainResponse.error ? (
164
+ <p className="text-red-400">{withBrainResponse.error}</p>
165
+ ) : (
166
+ <>
167
+ <div>
168
+ <h3 className="text-primary/70 text-sm font-bold uppercase tracking-wider mb-1">
169
+ Recommended Action
170
+ </h3>
171
+ <p className="text-xl font-semibold text-white bg-primary/10 p-4 border border-primary/30 rounded">
172
+ {withBrainResponse.recommended_action}
173
+ </p>
174
+ </div>
175
+
176
+ <div className="grid grid-cols-2 gap-4">
177
+ <div>
178
+ <h3 className="text-primary/70 text-sm font-bold uppercase tracking-wider mb-1">
179
+ Skill Matched
180
+ </h3>
181
+ <p className="font-mono text-sm bg-background p-2 rounded">
182
+ {withBrainResponse.skill_matched || "N/A"}
183
+ </p>
184
+ </div>
185
+ <div>
186
+ <h3 className="text-primary/70 text-sm font-bold uppercase tracking-wider mb-1">
187
+ Confidence
188
+ </h3>
189
+ <div className="flex items-center gap-2 mt-2">
190
+ <div className="flex-1 bg-background h-2 rounded-full overflow-hidden">
191
+ <div
192
+ className={`h-full ${confidenceColor(withBrainResponse.confidence || 0)}`}
193
+ style={{ width: `${(withBrainResponse.confidence || 0) * 100}%` }}
194
+ ></div>
195
+ </div>
196
+ <span className="text-xs font-mono">
197
+ {((withBrainResponse.confidence || 0) * 100).toFixed(0)}%
198
+ </span>
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ {/* Retrieval Scores */}
204
+ {withBrainResponse.retrieval_scores && withBrainResponse.retrieval_scores.length > 0 && (
205
+ <div>
206
+ <h3 className="text-primary/70 text-sm font-bold uppercase tracking-wider mb-1">
207
+ Retrieval Scores (Top {withBrainResponse.retrieval_scores.length} Skills)
208
+ </h3>
209
+ <div className="flex gap-2 flex-wrap">
210
+ {withBrainResponse.retrieval_scores.map((score, i) => (
211
+ <span
212
+ key={i}
213
+ className="bg-background border border-gray-700 px-2 py-1 rounded text-xs font-mono"
214
+ >
215
+ #{i + 1}: {(score * 100).toFixed(1)}%
216
+ </span>
217
+ ))}
218
+ </div>
219
+ </div>
220
+ )}
221
+
222
+ <div>
223
+ <h3 className="text-primary/70 text-sm font-bold uppercase tracking-wider mb-1">
224
+ Rule Applied
225
+ </h3>
226
+ <p className="text-white border-l-2 border-primary pl-3 py-1 font-medium">
227
+ {withBrainResponse.rule_applied}
228
+ </p>
229
+ </div>
230
+
231
+ {/* Reasoning */}
232
+ {withBrainResponse.reasoning && (
233
+ <div>
234
+ <h3 className="text-primary/70 text-sm font-bold uppercase tracking-wider mb-1">
235
+ LLM Reasoning
236
+ </h3>
237
+ <p className="text-sm text-gray-300 bg-background p-3 rounded border border-gray-800">
238
+ {withBrainResponse.reasoning}
239
+ </p>
240
+ </div>
241
+ )}
242
+
243
+ {withBrainResponse.evidence && withBrainResponse.evidence.length > 0 && (
244
+ <div>
245
+ <h3 className="text-primary/70 text-sm font-bold uppercase tracking-wider mb-2">
246
+ Evidence Trail
247
+ </h3>
248
+ <ul className="space-y-2">
249
+ {withBrainResponse.evidence.map((src, i) => (
250
+ <li key={i} className="text-gray-300 text-sm bg-background p-3 rounded border border-gray-800">
251
+ {src}
252
+ </li>
253
+ ))}
254
+ </ul>
255
+ </div>
256
+ )}
257
+ </>
258
+ )}
259
+ </div>
260
+ ) : (
261
+ <p>Loading...</p>
262
+ )}
263
+ </div>
264
+ </div>
265
+ )}
266
+ </div>
267
+ </div>
268
+ );
269
+ }
frontend/src/app/favicon.ico ADDED
frontend/src/app/globals.css ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #0A0F14;
5
+ --foreground: #E2E8F0;
6
+ --surface: #131B23;
7
+ --primary: #00D2B4;
8
+ --text-secondary: #94A3B8;
9
+ }
10
+
11
+ @theme inline {
12
+ --color-background: var(--background);
13
+ --color-foreground: var(--foreground);
14
+ --color-surface: var(--surface);
15
+ --color-primary: var(--primary);
16
+ --color-text-secondary: var(--text-secondary);
17
+ --font-sans: var(--font-geist-sans);
18
+ --font-mono: var(--font-geist-mono);
19
+ }
20
+
21
+ body {
22
+ background: var(--background);
23
+ color: var(--foreground);
24
+ }
frontend/src/app/layout.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
6
+ variable: "--font-geist-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "Create Next App",
17
+ description: "Generated by create next app",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html
27
+ lang="en"
28
+ className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
29
+ >
30
+ <body className="min-h-full flex flex-col">{children}</body>
31
+ </html>
32
+ );
33
+ }
frontend/src/app/page.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ export default function Dashboard() {
7
+ const [companyId, setCompanyId] = useState("");
8
+ const [loading, setLoading] = useState(false);
9
+ const router = useRouter();
10
+
11
+ const handleCompile = async () => {
12
+ if (!companyId) return;
13
+ setLoading(true);
14
+ try {
15
+ const res = await fetch("http://localhost:8080/compile", {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify({ company_id: companyId }),
19
+ });
20
+ const data = await res.json();
21
+ if (data.job_id) {
22
+ router.push(`/compile/${data.job_id}`);
23
+ }
24
+ } catch (err) {
25
+ console.error(err);
26
+ alert("Failed to start compilation");
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ const handleQuery = () => {
33
+ if (companyId) {
34
+ router.push(`/demo/${companyId}`);
35
+ }
36
+ };
37
+
38
+ const handleViewSkills = () => {
39
+ if (companyId) {
40
+ router.push(`/skills/${companyId}`);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <div className="min-h-screen p-8 flex flex-col items-center justify-center">
46
+ <div className="max-w-md w-full bg-surface p-8 border border-gray-800 shadow-2xl">
47
+ <h1 className="text-3xl font-bold text-primary mb-6">Kernl Compilation</h1>
48
+
49
+ <div className="mb-6">
50
+ <label className="block text-text-secondary text-sm font-bold mb-2">
51
+ Company ID
52
+ </label>
53
+ <input
54
+ type="text"
55
+ className="w-full px-3 py-2 bg-background border border-gray-700 text-foreground focus:outline-none focus:border-primary"
56
+ placeholder="e.g. comp_123"
57
+ value={companyId}
58
+ onChange={(e) => setCompanyId(e.target.value)}
59
+ />
60
+ </div>
61
+
62
+ <div className="flex flex-col gap-3">
63
+ <button
64
+ onClick={handleCompile}
65
+ disabled={loading || !companyId}
66
+ className="w-full bg-primary text-background font-bold py-2 px-4 hover:opacity-90 disabled:opacity-50"
67
+ >
68
+ {loading ? "Starting..." : "Compile Brain"}
69
+ </button>
70
+
71
+ <button
72
+ onClick={handleViewSkills}
73
+ disabled={!companyId}
74
+ className="w-full border border-primary text-primary font-bold py-2 px-4 hover:bg-primary/10 disabled:opacity-50"
75
+ >
76
+ View Skills File
77
+ </button>
78
+
79
+ <button
80
+ onClick={handleQuery}
81
+ disabled={!companyId}
82
+ className="w-full border border-gray-600 text-foreground font-bold py-2 px-4 hover:bg-gray-800 disabled:opacity-50"
83
+ >
84
+ Query Agent Demo
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
frontend/src/app/skills/[companyId]/page.tsx ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState, use } from "react";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ type Skill = {
7
+ id?: string;
8
+ category?: string;
9
+ rule?: string;
10
+ rationale?: string;
11
+ evidence?: string[];
12
+ confidence?: number;
13
+ };
14
+
15
+ type SkillsData = {
16
+ skills: Skill[];
17
+ version?: string;
18
+ compiled_at?: string;
19
+ brain_id?: string;
20
+ };
21
+
22
+ export default function SkillsViewer({ params }: { params: Promise<{ companyId: string }> }) {
23
+ const resolvedParams = use(params);
24
+ const companyId = resolvedParams.companyId;
25
+ const [data, setData] = useState<SkillsData | null>(null);
26
+ const [loading, setLoading] = useState(true);
27
+ const [filter, setFilter] = useState("");
28
+ const [sortBy, setSortBy] = useState<"category" | "confidence">("category");
29
+ const router = useRouter();
30
+
31
+ useEffect(() => {
32
+ fetch(`http://localhost:8080/skills/${companyId}`)
33
+ .then((res) => res.json())
34
+ .then((d) => {
35
+ setData(d);
36
+ setLoading(false);
37
+ })
38
+ .catch((err) => {
39
+ console.error(err);
40
+ setLoading(false);
41
+ });
42
+ }, [companyId]);
43
+
44
+ const skills = data?.skills || [];
45
+ const categories = [...new Set(skills.map((s) => s.category || "Unknown"))];
46
+
47
+ const filtered = skills
48
+ .filter((s) => {
49
+ if (!filter) return true;
50
+ return (s.category || "").toLowerCase().includes(filter.toLowerCase());
51
+ })
52
+ .sort((a, b) => {
53
+ if (sortBy === "confidence") return (b.confidence || 0) - (a.confidence || 0);
54
+ return (a.category || "").localeCompare(b.category || "");
55
+ });
56
+
57
+ const confidenceColor = (c: number) => {
58
+ if (c >= 0.8) return "text-green-400 border-green-400/30";
59
+ if (c >= 0.6) return "text-yellow-400 border-yellow-400/30";
60
+ if (c >= 0.4) return "text-orange-400 border-orange-400/30";
61
+ return "text-red-400 border-red-400/30";
62
+ };
63
+
64
+ return (
65
+ <div className="min-h-screen p-8 flex flex-col">
66
+ <div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
67
+ <div>
68
+ <h1 className="text-2xl font-bold text-primary">Skills File Viewer</h1>
69
+ {data?.version && (
70
+ <p className="text-text-secondary text-sm mt-1">
71
+ Version: <span className="font-mono text-primary">{data.version}</span>
72
+ {data.compiled_at && (
73
+ <> · Compiled: {new Date(data.compiled_at).toLocaleString()}</>
74
+ )}
75
+ {" · "}{skills.length} skills
76
+ </p>
77
+ )}
78
+ </div>
79
+ <button onClick={() => router.push("/")} className="text-text-secondary hover:text-foreground">
80
+ Back
81
+ </button>
82
+ </div>
83
+
84
+ {/* Filter + Sort Controls */}
85
+ <div className="flex gap-4 mb-4">
86
+ <select
87
+ value={filter}
88
+ onChange={(e) => setFilter(e.target.value)}
89
+ className="bg-surface border border-gray-700 text-foreground px-3 py-2 text-sm"
90
+ >
91
+ <option value="">All Categories</option>
92
+ {categories.map((c) => (
93
+ <option key={c} value={c}>{c}</option>
94
+ ))}
95
+ </select>
96
+ <select
97
+ value={sortBy}
98
+ onChange={(e) => setSortBy(e.target.value as "category" | "confidence")}
99
+ className="bg-surface border border-gray-700 text-foreground px-3 py-2 text-sm"
100
+ >
101
+ <option value="category">Sort by Category</option>
102
+ <option value="confidence">Sort by Confidence</option>
103
+ </select>
104
+ </div>
105
+
106
+ {/* Skills Grid */}
107
+ <div className="flex-1 overflow-y-auto">
108
+ {loading ? (
109
+ <div className="text-text-secondary">Loading skills...</div>
110
+ ) : filtered.length === 0 ? (
111
+ <div className="text-center py-12">
112
+ <p className="text-text-secondary text-lg">No skills compiled yet.</p>
113
+ <p className="text-text-secondary text-sm mt-2">
114
+ Go to Dashboard → Compile Brain to generate skills from your source documents.
115
+ </p>
116
+ </div>
117
+ ) : (
118
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
119
+ {filtered.map((skill, i) => (
120
+ <div
121
+ key={skill.id || i}
122
+ className="bg-surface border border-gray-800 p-5 hover:border-primary/30 transition-colors"
123
+ >
124
+ <div className="flex justify-between items-start mb-3">
125
+ <span className="text-xs font-mono bg-primary/10 text-primary px-2 py-1 rounded">
126
+ {skill.category || "Unknown"}
127
+ </span>
128
+ <span
129
+ className={`text-xs font-mono px-2 py-1 border rounded ${confidenceColor(
130
+ skill.confidence || 0
131
+ )}`}
132
+ >
133
+ {((skill.confidence || 0) * 100).toFixed(0)}%
134
+ </span>
135
+ </div>
136
+
137
+ <p className="text-white font-medium mb-2">{skill.rule}</p>
138
+
139
+ {skill.rationale && (
140
+ <p className="text-text-secondary text-sm mb-3 italic">{skill.rationale}</p>
141
+ )}
142
+
143
+ {skill.evidence && skill.evidence.length > 0 && (
144
+ <div className="border-t border-gray-800 pt-3 mt-3">
145
+ <h4 className="text-xs text-text-secondary uppercase tracking-wider mb-2">
146
+ Evidence ({skill.evidence.length})
147
+ </h4>
148
+ {skill.evidence.map((e, j) => (
149
+ <p key={j} className="text-xs text-gray-400 mb-1 pl-2 border-l border-gray-700">
150
+ {e}
151
+ </p>
152
+ ))}
153
+ </div>
154
+ )}
155
+ </div>
156
+ ))}
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+ );
162
+ }