GitHub Actions Bot commited on
Commit
985f3ee
Β·
0 Parent(s):

deploy: automated sync from github main

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. .github/workflows/deploy.yml +43 -0
  3. .gitignore +36 -0
  4. .opencode/opencode-loop/ses_04f15b806ffeVMcZMQ3SvTElIa.json +4 -0
  5. .opencode/opencode-loop/ses_04f15c7c7ffeptX8ZkC4eI4nwV.json +4 -0
  6. .opencode/opencode-loop/ses_06a20babdffeQ3bykyB3CydzUC.json +4 -0
  7. .opencode/opencode-loop/ses_06a20e31effezqv3Ypf6NlGToc.json +4 -0
  8. .python-version +1 -0
  9. AGENTS.md +128 -0
  10. Dockerfile +28 -0
  11. GOAL.MD +112 -0
  12. README.md +149 -0
  13. aruncore_master_blueprint.md +257 -0
  14. backend/README.md +64 -0
  15. backend/app/api/v1/config.py +13 -0
  16. backend/app/api/v1/router.py +9 -0
  17. backend/app/api/v1/voice.py +20 -0
  18. backend/app/api/v1/webhook.py +20 -0
  19. backend/app/core/README.md +52 -0
  20. backend/app/core/__init__.py +0 -0
  21. backend/app/core/agent.py +136 -0
  22. backend/app/core/api.py +207 -0
  23. backend/app/core/bot.py +134 -0
  24. backend/app/core/evaluate.py +171 -0
  25. backend/app/core/ingest.py +279 -0
  26. backend/app/main.py +15 -0
  27. backend/app/schemas/chat.py +30 -0
  28. backend/app/schemas/tenant.py +122 -0
  29. backend/app/schemas/voice.py +8 -0
  30. backend/app/schemas/webhook.py +15 -0
  31. backend/app/services/active_learning_service.py +18 -0
  32. backend/app/services/agent_runner.py +428 -0
  33. backend/app/services/auth_service.py +22 -0
  34. backend/app/services/background.py +49 -0
  35. backend/app/services/knowledge_service.py +453 -0
  36. backend/app/services/memory_manager.py +69 -0
  37. backend/app/services/notification_service.py +508 -0
  38. backend/app/services/prompt_builder.py +162 -0
  39. backend/app/services/rag_service.py +48 -0
  40. backend/app/services/session_store.py +121 -0
  41. backend/app/services/tenant_service.py +99 -0
  42. backend/app/services/tool_executor.py +92 -0
  43. backend/app/services/voice_service.py +39 -0
  44. data/HOW_TO_UPDATE_DATA.md +51 -0
  45. data/README.md +37 -0
  46. data/github/01_manage_patient_task/README.md +87 -0
  47. data/github/01_manage_patient_task/metadata.json +10 -0
  48. data/github/Agentic_AI_Projects/README.md +21 -0
  49. data/github/Agentic_AI_Projects/metadata.json +10 -0
  50. data/github/ArunCore/README.md +16 -0
.gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ *.bin filter=lfs diff=lfs merge=lfs -text
2
+ *.sqlite3 filter=lfs diff=lfs merge=lfs -text
3
+ *.png filter=lfs diff=lfs merge=lfs -text
4
+ *.jpg filter=lfs diff=lfs merge=lfs -text
5
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
.github/workflows/deploy.yml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-huggingface:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout Repository
13
+ uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+
17
+ - name: Push to Hugging Face Space
18
+ env:
19
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
20
+ run: |
21
+ git config --global user.name "GitHub Actions Bot"
22
+ git config --global user.email "actions@github.com"
23
+ git checkout --orphan hf-deploy-temp
24
+ git rm -rf --cached . 2>/dev/null || true
25
+ git add -A
26
+ git reset -- Images/ db/ *.png *.jpg *.jpeg 2>/dev/null || true
27
+ git commit -m "deploy: automated sync from github main" || true
28
+ git push --force https://neural-arun:${HF_TOKEN}@huggingface.co/spaces/neural-arun/ArunCore hf-deploy-temp:main
29
+
30
+ - name: Restart Hugging Face Space
31
+ env:
32
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
33
+ run: |
34
+ echo "Triggering Space restart to apply new code..."
35
+ STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
36
+ -X POST "https://huggingface.co/api/spaces/neural-arun/ArunCore/restart?factory=true" \
37
+ -H "Authorization: Bearer ${HF_TOKEN}")
38
+ echo "HF Restart API response: $STATUS"
39
+ if [ "$STATUS" = "200" ] || [ "$STATUS" = "204" ]; then
40
+ echo "βœ… Space restart triggered successfully!"
41
+ else
42
+ echo "⚠️ Restart returned status $STATUS β€” Space may need a manual restart."
43
+ fi
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Keys and Secrets
2
+ .env
3
+
4
+ # Python Environment
5
+ venv/
6
+ .venv/
7
+ env/
8
+ *.py[cod]
9
+ __pycache__/
10
+ .pytest_cache/
11
+
12
+
13
+
14
+ # Evaluation Logs and Debugging
15
+ evaluation_debug/
16
+ temp_error.txt
17
+ error.txt
18
+ data/test_set/evaluation_report.json
19
+
20
+ # IDE Settings
21
+ .vscode/
22
+ .idea/
23
+
24
+ # OS Junk
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ .vercel
29
+
30
+ # Database Files
31
+ db/
32
+
33
+ # Image Artifacts
34
+ *.png
35
+ *.jpg
36
+ *.jpeg
.opencode/opencode-loop/ses_04f15b806ffeVMcZMQ3SvTElIa.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 4,
3
+ "jobs": []
4
+ }
.opencode/opencode-loop/ses_04f15c7c7ffeptX8ZkC4eI4nwV.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 4,
3
+ "jobs": []
4
+ }
.opencode/opencode-loop/ses_06a20babdffeQ3bykyB3CydzUC.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 4,
3
+ "jobs": []
4
+ }
.opencode/opencode-loop/ses_06a20e31effezqv3Ypf6NlGToc.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 4,
3
+ "jobs": []
4
+ }
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
AGENTS.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ€– AGENTS.md β€” Master Protocol & Execution Rules for AI Coding Agents
2
+
3
+ > **IMPORTANT**: Any coding agent or LLM-assisted development tool (Antigravity, Claude, Cursor, Copilot, etc.) working on this repository **MUST** read this file and follow every rule strictly.
4
+ > **ARCHITECTURE VERSION**: ❄️ **v1.0 FROZEN** (Official Release Specification)
5
+
6
+ ---
7
+
8
+ ## 🎯 Project Mission
9
+ ArunCore is a domain-generic, multi-tenant enterprise AI platform.
10
+ Your primary objective is **NOT** to maximize code generation.
11
+ Your primary objective is to **preserve architectural integrity by default while implementing the smallest correct change**.
12
+ If a requested feature requires architectural changes, explain the tradeoffs and ask for user confirmation before proceeding.
13
+
14
+ ---
15
+
16
+ ## πŸ” Priority Order (Highest β†’ Lowest)
17
+ When resolving conflicts, follow this exact hierarchy:
18
+ 1. πŸ‘€ **User explicit instructions**
19
+ 2. πŸ“„ **AGENTS.md** (Master Protocol & Rules)
20
+ 3. πŸ“ **aruncore_master_blueprint.md** (Architecture Specification)
21
+ 4. πŸ“‚ **understanding_each_folder/** (Technical Folder Guides)
22
+ 5. βš™οΈ **Existing Codebase**
23
+
24
+ ---
25
+
26
+ ## πŸ“œ File Reading Priority
27
+ Always inspect files in this exact hierarchy before modifying code:
28
+ 1. πŸ“„ Master Architecture Spec: [`aruncore_master_blueprint.md`](file:///home/arun/projects/profile/aruncore_master_blueprint.md)
29
+ 2. πŸ“‚ Folder Technical Guides: [`understanding_each_folder/README.md`](file:///home/arun/projects/profile/understanding_each_folder/README.md)
30
+ 3. πŸ“‹ Validation Schemas: `backend/app/schemas/` (`tenant.py`, `chat.py`)
31
+ 4. βš™οΈ Source Code: `backend/app/` or `frontend/`
32
+ 5. πŸ§ͺ Test Suite: `tests/`
33
+
34
+ ---
35
+
36
+ ## πŸ›οΈ The 11 Golden Architecture Principles (NON-NEGOTIABLE)
37
+
38
+ 1. πŸ₯‡ **Preserve Current Frontend 100%**: Absolutely DO NOT replace or alter the existing Next.js frontend UI (`frontend/app/page.tsx`, `frontend/components/ChatPanel.tsx`, `ProjectsView.tsx`, `ManifestoView.tsx`, `Header.tsx`). The existing luxury UI, design tokens, glowing accent borders, light/dark themes, hero assistant card, and tab views stay 100% intact!
39
+ 2. πŸ₯ˆ **Zero Functionality Loss (Keep & Enhance)**: DO NOT remove a single working feature from the current app (Hybrid RAG, Telegram active learning alerts, 3-way live human chat presence, TTS neural voice, admin mode, session history). The goal is to keep 100% of existing functionality and upgrade it to production grade!
40
+ 3. πŸ₯‰ **Production-Grade Hybrid RAG Engine**: Maintain and enhance the hybrid RAG architecture (dense ChromaDB vector search + sparse BM25 keyword search + Cohere/LLM reranking) for ultra-fast, high-precision knowledge retrieval per tenant.
41
+ 4. πŸ… **Ultra-Simple Demo System**: Demo mode operates by passing a URL query parameter (`?tutor=ed_donner` or `?client=ed_donner`). The frontend fetches metadata from `/api/v1/config?tutor=ed_donner` (or `/config?tutor=ed_donner`), which dynamically populates the existing hero card title, role subtitle, avatar, welcome text, suggested questions, and AI system prompt. Zero complex slot engines required!
42
+ 5. πŸ… **Explicit `tenants/` Directory**: All raw PDFs, markdown files, avatars, logos, and vector databases live in `./tenants/` outside the Git code repo. Keep code repos lightweight (~10MB).
43
+ 6. πŸ… **Split Targeted Config**: Tenant configs in `tenants/<id>/config/` are split into 6 targeted JSON files (`brand.json`, `agent.json`, `chat.json`, `voice.json`, `seo.json`, `social.json`). Easy debugging, zero merge conflicts, Pydantic validated.
44
+ 7. πŸ… **ZERO Backend Client `if` Statements**: Absolutely NO `if client == "ed":` or `if tenant_id == "hitesh":` statements anywhere in Python code! All tenant logic, prompts, tools, and branding MUST be resolved dynamically by `TenantService`.
45
+ 8. πŸ… **Config-Driven Tool Registry**: Client tools are controlled via the `enabled_tools` array in `agent.json` (`["search_courses", "book_calendar", "faq_lookup"]`). `ToolExecutor` dynamically binds enabled tools into the LLM execution loopβ€”zero Python code edits required!
46
+ 9. πŸ… **Separation of Tenant Assets**: Static brand images (`tenants/<id>/assets/avatars/`, `tenants/<id>/assets/logos/`) are kept separate from code logic.
47
+ 10. πŸ… **Decoupled Single-Responsibility Services**: Maintain small, focused backend services (`PromptBuilder`, `MemoryManager`, `ToolExecutor`, `AgentRunner`, `RAGService`, `NotificationService`, `ActiveLearningService`). Do NOT create monster monolithic service files.
48
+ 11. πŸ… **Interface Abstractions**: Use abstract base classes for adapters (`VectorStore`, `StateStore`, `NotificationProvider`). Never hardcode direct dependencies in business logic.
49
+
50
+ ---
51
+
52
+ ## βš™οΈ Required Execution Workflow
53
+
54
+ 1. **Understand**: Read the task and relevant architecture docs.
55
+ 2. **Locate**: Identify the exact affected files.
56
+ 3. **Plan**: If the change affects >5 files, public APIs, schemas, or architecture -> create a plan first. Do NOT write code immediately.
57
+ 4. **Minimal Change**: Apply the smallest possible correct change. Prefer modifying existing code over spawning new modules.
58
+ 5. **Verify**: Run automated tests (`python3 -m unittest discover -s tests`).
59
+ 6. **Update Docs**: Immediately update both `aruncore_master_blueprint.md` AND `understanding_each_folder/` guides.
60
+ 7. **Self-Review**: Review your diff against the pre-completion checklist.
61
+
62
+ ---
63
+
64
+ ## ❓ Ask Before Proceeding
65
+
66
+ Always ask for user confirmation before:
67
+ - Deleting files or modules.
68
+ - Renaming large directories.
69
+ - Changing public API contracts or endpoints.
70
+ - Modifying validation schemas or database structures.
71
+ - Changing authentication, security, or permissions logic.
72
+ - Modifying package dependencies or build tooling.
73
+
74
+ ---
75
+
76
+ ## πŸ›‘ When NOT to Code (Escalation Triggers)
77
+
78
+ Stop coding immediately, explain the conflict, and ask for clarification when:
79
+ - Architecture or requirements are unclear.
80
+ - API contracts or schemas are missing.
81
+ - The request contradicts any Golden Principle or blueprint specification.
82
+
83
+ ---
84
+
85
+ ## ❌ "Never Do These" (Strict Ban List)
86
+
87
+ Never:
88
+ - Alter or replace the existing Next.js frontend UI layout, headers, tabs, or design system.
89
+ - Remove or degrade existing working features (RAG, Telegram alerts, TTS, active learning loop, admin mode).
90
+ - Invent APIs, schemas, environment variables, endpoints, or tenant configs without authoritative source definitions.
91
+ - Write hardcoded `if client == "..."` logic anywhere in Python code.
92
+ - Wrap failing logic in silent `try/except: pass` or return dummy empty fallbacks.
93
+ - Skip verification commands or leave TODO implementations.
94
+ - Rename large directories, reformat unrelated files, or modify package lockfiles unless explicitly requested.
95
+ - Hardcode secrets, log tokens/API keys, or disable validation/authentication.
96
+ - Introduce N+1 database queries, blocking I/O on main loops, or redundant vector searches.
97
+
98
+ ---
99
+
100
+ ## πŸ”’ Minimal Change Principle
101
+ - Prefer modifying existing code over creating new abstractions.
102
+ - Only create new modules or files when strictly necessary. Avoid spawning unnecessary files.
103
+
104
+ ---
105
+
106
+ ## βœ… Definition of "DONE"
107
+
108
+ A task is complete ONLY if:
109
+ - [ ] Code compiles and builds cleanly without warnings.
110
+ - [ ] Existing Next.js frontend UI is 100% preserved and functional.
111
+ - [ ] 100% of existing capabilities (Hybrid RAG, Telegram active learning, TTS, 3-way chat) remain fully intact.
112
+ - [ ] Verification tests pass (`python3 -m unittest discover -s tests`).
113
+ - [ ] Both `aruncore_master_blueprint.md` AND `understanding_each_folder/` guides are fully updated.
114
+ - [ ] No Golden Architecture Principles are violated.
115
+ - [ ] No dead code, commented-out blocks, or unused imports remain.
116
+
117
+ ---
118
+
119
+ ## πŸ“‹ Pre-Completion Review Checklist
120
+
121
+ Before marking any task complete, verify:
122
+ - [ ] **Frontend Preserved**: Existing UI layout, tabs (`Chat`, `Projects`, `Manifesto`), design tokens, and components are 100% intact.
123
+ - [ ] **Features Retained**: Zero feature removals (Hybrid RAG, Telegram alerts, TTS, active learning loop preserved).
124
+ - [ ] **Architecture preserved**: Zero `if client == ...` checks, clean backend decoupling.
125
+ - [ ] **Minimal diff**: No unrelated files edited or reformatted.
126
+ - [ ] **Type safety**: Pydantic validation used in backend; explicit TypeScript interfaces used in frontend.
127
+ - [ ] **Documentation synced**: Blueprint and folder guides match the diff.
128
+ - [ ] **Tests verified**: Test suite passed with zero errors.
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies & Node.js for frontend static export
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ build-essential \
9
+ curl \
10
+ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
11
+ && apt-get install -y nodejs \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Copy requirements and install python dependencies
15
+ COPY requirements.txt .
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy the entire project
19
+ COPY . .
20
+
21
+ # Build frontend static export
22
+ RUN cd frontend && npm install && npm run build
23
+
24
+ # Expose standard container port
25
+ EXPOSE 8000
26
+
27
+ # Run FastAPI server (which serves the mounted frontend and API endpoints)
28
+ CMD ["sh", "-c", "if [ \"$RUN_TELEGRAM_PUBLIC_BOT\" = \"true\" ]; then python -m backend.app.core.bot & fi; uvicorn backend.app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
GOAL.MD ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎯 GOAL.MD β€” ArunCore Production Upgrade Roadmap & Master Plan
2
+
3
+ > **STATUS**: πŸ“œ **MASTER EXECUTION SPECIFICATION**
4
+ > **PROJECT**: **ArunCore Multi-Tenant Enterprise AI Platform**
5
+
6
+ ---
7
+
8
+ ## πŸ“‹ Executive Objective & Mission Statement
9
+
10
+ The primary goal of this project is to upgrade **ArunCore** into a production-grade, domain-generic, multi-tenant enterprise AI platform while adhering strictly to three non-negotiable directives:
11
+
12
+ 1. πŸ₯‡ **100% Frontend Preservation**: The existing Next.js frontend UI layout, components, header, tabs (`ChatPanel`, `ProjectsView`, `ManifestoView`), glowing accent borders, light/dark themes, hero assistant card, and design tokens stay **100% INTACT**. No layout rewrites or UI structural replacements will take place.
13
+ 2. πŸ₯ˆ **Zero Functionality Loss (Keep & Enhance Everything)**: Absolutely **NO** working features will be removed from the current app. All existing capabilitiesβ€”including the Hybrid RAG engine, Telegram active learning alerts, 3-way live human chat presence notice, TTS neural voice endpoints, admin mode, and session historyβ€”will be preserved 100% and upgraded for production-grade scaling!
14
+ 3. πŸ₯‰ **Production-Grade Hybrid RAG Engine**: The RAG retrieval pipeline (dense ChromaDB vector search + sparse BM25 keyword search + Cohere/LLM reranking) will be enhanced to deliver hyper-fast, accurate, context-aware answers per tenant.
15
+ 4. πŸ… **Ultra-Simple Demo Mode (`?tutor=ed_donner` / `?client=ed_donner`)**: Demo mode operates by passing a URL query parameter (`?tutor=ed_donner`). The frontend fetches metadata from `/api/v1/config?tutor=ed_donner` (or `/config?tutor=ed_donner`), which dynamically populates the existing hero card title, role subtitle, avatar, welcome text, suggested questions, and AI system prompt.
16
+
17
+ ---
18
+
19
+ ## πŸ›οΈ The 11 Golden Architecture Principles (NON-NEGOTIABLE)
20
+
21
+ 1. πŸ₯‡ **Preserve Current Frontend 100%**: Absolutely DO NOT replace or alter the existing Next.js frontend UI (`frontend/app/page.tsx`, `frontend/components/ChatPanel.tsx`, `ProjectsView.tsx`, `ManifestoView.tsx`, `Header.tsx`). The existing luxury UI, design tokens, glowing accent borders, light/dark themes, hero assistant card, and tab views stay 100% intact!
22
+ 2. πŸ₯ˆ **Zero Functionality Loss (Keep & Enhance)**: DO NOT remove a single working feature from the current app (Hybrid RAG, Telegram active learning alerts, 3-way live human chat presence, TTS neural voice, admin mode, session history). The goal is to keep 100% of existing functionality and upgrade it to production grade!
23
+ 3. πŸ₯‰ **Production-Grade Hybrid RAG Engine**: Maintain and enhance the hybrid RAG architecture (dense ChromaDB vector search + sparse BM25 keyword search + Cohere/LLM reranking) for ultra-fast, high-precision knowledge retrieval per tenant.
24
+ 4. πŸ… **Ultra-Simple Demo System**: Demo mode operates by passing a URL query parameter (`?tutor=ed_donner` or `?client=ed_donner`). The frontend fetches metadata from `/api/v1/config?tutor=ed_donner` (or `/config?tutor=ed_donner`), which dynamically populates the existing hero card title, role subtitle, avatar, welcome text, suggested questions, and AI system prompt. Zero complex slot engines required!
25
+ 5. πŸ… **Explicit `tenants/` Directory**: All raw PDFs, markdown files, avatars, logos, and vector databases live in `./tenants/` outside the Git code repo. Keep code repos lightweight (~10MB).
26
+ 6. πŸ… **Split Targeted Config**: Tenant configs in `tenants/<id>/config/` are split into 6 targeted JSON files (`brand.json`, `agent.json`, `chat.json`, `voice.json`, `seo.json`, `social.json`). Easy debugging, zero merge conflicts, Pydantic validated.
27
+ 7. πŸ… **ZERO Backend Client `if` Statements**: Absolutely NO `if client == "ed":` or `if tenant_id == "hitesh":` statements anywhere in Python code! All tenant logic, prompts, tools, and branding MUST be resolved dynamically by `TenantService`.
28
+ 8. πŸ… **Config-Driven Tool Registry**: Client tools are controlled via the `enabled_tools` array in `agent.json` (`["search_courses", "book_calendar", "faq_lookup"]`). `ToolExecutor` dynamically binds enabled tools into the LLM execution loopβ€”zero Python code edits required!
29
+ 9. πŸ… **Separation of Tenant Assets**: Static brand images (`tenants/<id>/assets/avatars/`, `tenants/<id>/assets/logos/`) are kept separate from code logic.
30
+ 10. πŸ… **Decoupled Single-Responsibility Services**: Maintain small, focused backend services (`PromptBuilder`, `MemoryManager`, `ToolExecutor`, `AgentRunner`, `RAGService`, `NotificationService`, `ActiveLearningService`). Do NOT create monster monolithic service files.
31
+ 11. πŸ… **Interface Abstractions**: Use abstract base classes for adapters (`VectorStore`, `StateStore`, `NotificationProvider`). Never hardcode direct dependencies in business logic.
32
+
33
+ ---
34
+
35
+ ## πŸ› οΈ Step-by-Step Execution Plan (Detailed Roadmap)
36
+
37
+ ```
38
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
39
+ β”‚ Detailed Step-by-Step Process β”‚
40
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
41
+ β”‚ STEP 1 β”‚ STEP 2 β”‚ STEP 3 β”‚ STEP 4 β”‚ STEP 5 β”‚
42
+ β”‚ tenants/ β”‚ Pydantic β”‚ Decoupled β”‚ Versioned β”‚ Testing & β”‚
43
+ β”‚ Directory & β”‚ Validation β”‚ Services β”‚ API Routers β”‚ Verification β”‚
44
+ β”‚ Split Configsβ”‚ Schemas β”‚ Architecture β”‚ (/api/v1/) β”‚ β”‚
45
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
46
+ ```
47
+
48
+ ### πŸ“ Step 1: Explicit `tenants/` Directory & Split Targeted Configs
49
+ - Create `./tenants/` root directory structure outside Git.
50
+ - Build `tenants/tenant_starter/` and `tenants/ed_donner/` with 6 split JSON configs:
51
+ - `brand.json`: Hero title, role subtitle, avatar path, accent color.
52
+ - `agent.json`: System prompt, guardrails, `enabled_tools` array.
53
+ - `chat.json`: Welcome message, suggested question chips.
54
+ - `voice.json`: TTS alloy voice specs.
55
+ - `seo.json`: Meta title & description.
56
+ - `social.json`: LinkedIn, X, Udemy links.
57
+
58
+ ### πŸ“ Step 2: Pydantic Validation Schemas (`backend/app/schemas/`)
59
+ - Create Pydantic models:
60
+ - `tenant.py`: Validates all 6 tenant JSON configs on load.
61
+ - `chat.py`: Validates `ChatRequest`, streaming NDJSON chunks, session history.
62
+ - `webhook.py`: Validates Telegram/WhatsApp active learning replies.
63
+ - `voice.py`: Validates `TTSRequest` and STT response schemas.
64
+
65
+ ### πŸ“ Step 3: Decoupled Single-Responsibility Services (`backend/app/services/`)
66
+ - Build decoupled Python services preserving 100% of existing functionality:
67
+ - `tenant_service.py`: Dynamic loader for `tenants/<id>/config/` with **ZERO backend `if client == ...` statements**.
68
+ - `prompt_builder.py`: Synthesizes dynamic system prompts, guardrails, bio rules, and 3-way live human chat presence notices.
69
+ - `memory_manager.py`: Manages rolling chat session window context.
70
+ - `tool_executor.py`: Dynamic sandbox for tools specified in `agent.json` (`enabled_tools`).
71
+ - `agent_runner.py`: Handles 100% AI agent control and token-by-token streaming response generation.
72
+ - `rag_service.py`: Hybrid ChromaDB vector search + BM25 keyword search coordinator with reranking.
73
+ - `notification_service.py`: Async Telegram & WhatsApp alert dispatcher.
74
+ - `active_learning_service.py`: Real-time Q&A vector re-ingestion service.
75
+
76
+ ### πŸ“ Step 4: Interface Adapters (`backend/app/db/`)
77
+ - Implement abstract base classes and adapters:
78
+ - `interfaces.py`: Abstract classes (`VectorStore`, `StateStore`, `NotificationProvider`).
79
+ - `vectorstore.py`: ChromaDB + BM25 hybrid implementation.
80
+ - `state_store.py`: Thread-safe session memory store implementation.
81
+
82
+ ### πŸ“ Step 5: Versioned API Routers (`backend/app/api/v1/`)
83
+ - Implement FastAPI versioned routers:
84
+ - `router.py`: Master v1 aggregator.
85
+ - `chat.py`: Streaming `/api/v1/chat`, `/chat/history`, `/chat/human-messages`.
86
+ - `config.py`: Dynamic metadata endpoint `/api/v1/config` (powers `?tutor=ed_donner`).
87
+ - `webhook.py`: Telegram & WhatsApp active learning reply webhooks.
88
+ - `voice.py`: `/api/v1/voice/tts` neural audio generation.
89
+
90
+ ### πŸ“ Step 6: Frontend Metadata Wiring & Demo Mode Integration
91
+ - Connect Next.js frontend (`frontend/app/page.tsx`) to fetch metadata from `/api/v1/config?tutor=ed_donner`.
92
+ - Dynamically update hero title, role subtitle, avatar, welcome text, suggested questions, and AI prompt while **preserving 100% of the existing luxury UI layout, tabs, and components**.
93
+
94
+ ### πŸ“ Step 7: Automated Test Suite & Verification (`tests/`)
95
+ - Create test suite in `tests/`:
96
+ - `test_schemas.py`: Verifies Pydantic schema validation.
97
+ - `test_tenant_service.py`: Verifies dynamic tenant loading.
98
+ - `test_chat_api.py`: Verifies streaming responses and endpoint contracts.
99
+ - Gather concrete empirical runtime verification.
100
+
101
+ ---
102
+
103
+ ## 🎯 Verification Criteria for Success (Definition of "DONE")
104
+
105
+ - [ ] Existing Next.js frontend UI layout, components, tabs, and design system are 100% preserved.
106
+ - [ ] 100% of current functionality (Hybrid RAG, Telegram active learning alerts, TTS, 3-way live human chat, admin mode) remains fully functional.
107
+ - [ ] Hybrid RAG retrieval (ChromaDB + BM25 + Cohere reranking) is enhanced and production-ready per tenant.
108
+ - [ ] Demo mode (`?tutor=ed_donner`) dynamically populates hero card metadata and AI system prompt.
109
+ - [ ] Backend contains ZERO `if client == "..."` checks anywhere in Python code.
110
+ - [ ] `tenants/` storage structure holds isolated client data outside Git code repo.
111
+ - [ ] All automated tests pass (`python3 -m unittest discover -s tests`).
112
+ - [ ] `aruncore_master_blueprint.md` and `AGENTS.md` match the diff.
README.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ArunCore Enterprise AI Platform & Digital Twin System
3
+ emoji: 🧠
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # 🧠 ArunCore β€” Domain-Generic Multi-Tenant Enterprise AI Platform & Digital Twin
12
+
13
+ > **Architecture Spec Version:** ❄️ **v1.0 FROZEN**
14
+ > **Engine:** `gpt-4.1-nano` | **RAG:** ChromaDB (Dense) + BM25 (Sparse) + Cohere V3 (Reranker)
15
+ > **Frontend:** Next.js 16 (App Router) Luxury Dark/Light UI
16
+ > **Backend:** FastAPI Decoupled Microservices
17
+ > **Evaluation:** Automated 30-Question Multi-Turn ReAct Stress-Test Suite
18
+
19
+ ---
20
+
21
+ ## 🎯 Platform Overview
22
+
23
+ **ArunCore** is a domain-generic, multi-tenant enterprise AI platform and stateful personal digital twin built for **Arun Yadav** (AI Systems Architect specializing in Healthcare & Education).
24
+
25
+ The platform serves dual core functions:
26
+ 1. **Personal AI Digital Twin**: A 24/7 interactive representative of Arun Yadav, providing deep technical answers about his AI architectures, healthcare/education projects, GitHub repositories, and LinkedIn publications, backed by instant lead capture and dual Telegram alert capabilities.
27
+ 2. **Multi-Tenant Enterprise JSON Engine (`?tutor=<tutor_id>`)**: A zero-code multi-tenant advisor system capable of instantly spinning up bespoke 24/7 AI Course Advisors and Sales Representatives for any client or instructor (e.g. Ed Donner) via a single 238-key enterprise JSON schema dictionary.
28
+
29
+ ---
30
+
31
+ ## πŸ›οΈ Core Architecture Principles (Golden Rules)
32
+
33
+ 1. **Preserve Frontend 100%**: Next.js luxury UI (`app/page.tsx`, `ChatPanel.tsx`, `ProjectsView.tsx`, `ManifestoView.tsx`, `Header.tsx`) with glowing accent borders, light/dark themes, hero card, and tab views stays 100% intact.
34
+ 2. **Zero Functionality Loss**: Retains Hybrid RAG, active Telegram alerts, 3-way live human chat takeover, TTS neural voice studio, and evaluation harness.
35
+ 3. **Production-Grade Hybrid RAG Engine**: ChromaDB dense vector search + BM25 sparse keyword search + Cohere English V3 reranking for high-precision, zero-hallucination retrieval.
36
+ 4. **Dynamic JSON Config Engine**: Zero hardcoded `if client == "ed"` logic in Python or TypeScript. 100% of tenant branding, prompts, tools, headers, and catalogs resolve dynamically from JSON dictionaries (`demos/<tutor_id>_enterprise_dictionary.json`).
37
+ 5. **Config-Driven Tool Registry**: Client tools are controlled via `enabled_tools` array in config (`["search_arun_knowledge", "get_github_live_data", "notify_arun"]`).
38
+ 6. **Decoupled Backend Services**: Monoliths are banned; small single-responsibility services (`PromptBuilder`, `MemoryManager`, `ToolExecutor`, `RAGService`, `TenantService`, `VoiceService`, `ActiveLearningService`).
39
+
40
+ ---
41
+
42
+ ## ⚑ Key Technical Capabilities
43
+
44
+ ### 1. πŸ€– 7-Iteration Recursive Agentic Loop (`gpt-4.1-nano`)
45
+ - Powered by `gpt-4.1-nano` with up to **7 recursive tool execution turns** (`search_arun_knowledge`, `get_github_live_data`, `notify_arun`) per turn.
46
+ - Executes multi-step technical comparisons, live GitHub commit fetches, and knowledge base retrievals before generating answers.
47
+
48
+ ### 2. πŸ“š 2-Step GitHub & Knowledge Base Retrieval Pipeline
49
+ - Automatically fetches the 3 most recently updated repositories and live commit logs from GitHub API (`get_github_live_data`).
50
+ - Automatically queries local README architectures (`search_arun_knowledge`) for each returned repository name to provide deep technical details and direct clickable URLs.
51
+
52
+ ### 3. πŸ“Š Markdown Table & Direct Project Link Mandate
53
+ - Presents all multi-system comparisons (e.g. MedCoach vs NEET Bot vs Legal RAG) in clean **Markdown Tables**.
54
+ - Embeds direct, clickable GitHub URLs (`https://github.com/neural-arun/<repo>`) directly inside table headers and project descriptions.
55
+
56
+ ### 4. πŸ’Ό LinkedIn Insights & Social Engagement CTA
57
+ - Integrates scraped public LinkedIn posts (`data/linkedin/posts.md`) covering AI workforce trends (2026–2036), NEET CBT practice ecosystem, FastAPI Todo API, and BPSC Rank 5 updates.
58
+ - Appends direct clickable LinkedIn post URLs and natural social engagement calls-to-action inviting visitors to like, comment, or share their thoughts.
59
+
60
+ ### 5. 🚨 Dual-Channel Telegram Alerts & 3-Way Live Human Chat Takeover
61
+ - **Active Learning Logger**: Silently logs all chats to `@ai_twin_alert_bot`.
62
+ - **Urgent Lead Alert Bot**: Instantly alerts Arun's phone for hiring leads, unknown questions, or urgent inquiries with a **1-Click Magic Join Link** for real-time 3-way human chat takeover.
63
+
64
+ ---
65
+
66
+ ## πŸ“ Repository Directory Map
67
+
68
+ ```text
69
+ profile/
70
+ β”œβ”€β”€ backend/ # Python FastAPI Backend Architecture
71
+ β”‚ └── app/
72
+ β”‚ β”œβ”€β”€ api/v1/ # API Endpoints (/config, /chat, /voice, /webhook)
73
+ β”‚ β”œβ”€β”€ core/ # Core Orchestration (agent.py, api.py, bot.py, ingest.py)
74
+ β”‚ β”œβ”€β”€ schemas/ # Pydantic Schemas (tenant.py, chat.py, voice.py)
75
+ β”‚ β”œβ”€β”€ services/ # Decoupled Business Logic Services
76
+ β”‚ └��─ main.py # FastAPI Application Entrypoint
77
+ β”œβ”€β”€ frontend/ # Next.js 16 (App Router) Luxury UI
78
+ β”‚ β”œβ”€β”€ app/ # Page Routes & Global Styles
79
+ β”‚ β”œβ”€β”€ components/ # UI Components (ChatPanel, Header, ProjectsView, etc.)
80
+ β”‚ └── public/ # Static Avatars & Logos
81
+ β”œβ”€β”€ data/ # Data Stores & Knowledge Assets (Excluded from Git code bloat)
82
+ β”‚ β”œβ”€β”€ github/ # Readme files for all 21 public GitHub repos
83
+ β”‚ β”œβ”€β”€ linkedin/ # Scraped LinkedIn posts (posts.md)
84
+ β”‚ β”œβ”€β”€ raw/ # Personal background & unknown questions DB
85
+ β”‚ └── static/ # Public profile, rules of engagement, voice persona
86
+ β”œβ”€β”€ db/ # ChromaDB Vector Database & Ingestion State
87
+ β”œβ”€β”€ demos/ # Enterprise 238-Key Monolithic JSON Schemas
88
+ β”‚ β”œβ”€β”€ ed_donner_enterprise_dictionary.json
89
+ β”‚ β”œβ”€β”€ general.json # Master 238-key JSON Template
90
+ β”‚ └── master_enterprise_dictionary.json
91
+ β”œβ”€β”€ scripts/ # Automated Maintenance & Evaluation Suite
92
+ β”‚ β”œβ”€β”€ evaluate.py # 30-Question Multi-Turn ReAct Test Harness
93
+ β”‚ β”œβ”€β”€ evaluation_questions.md # 30 Test Questions List
94
+ β”‚ β”œβ”€β”€ evaluation_results.md # Generated Output & Execution Traces
95
+ β”‚ β”œβ”€β”€ ingest.py # ChromaDB Re-Ingestion Script
96
+ β”‚ β”œβ”€β”€ sync_github.py # GitHub API Auto-Sync Script
97
+ β”‚ β”œβ”€β”€ sync_linkedin.py # LinkedIn Posts Sync Script
98
+ β”‚ └── sync_all.py # 1-Click Master Data Sync Runner
99
+ β”œβ”€β”€ tenants/ # Multi-Tenant 6-File JSON Configurations
100
+ β”œβ”€β”€ tests/ # Automated Unit & Integration Test Suite
101
+ β”œβ”€β”€ AGENTS.md # Master Protocol & Execution Rules for AI Coding Agents
102
+ └── aruncore_master_blueprint.md # Complete Architecture Specification Doc
103
+ ```
104
+
105
+ ---
106
+
107
+ ## πŸš€ Quick Start & Local Execution
108
+
109
+ ### 1. Environment Setup
110
+ Ensure `.env` exists in root with valid API keys:
111
+ ```env
112
+ OPENAI_API_KEY=sk-proj-...
113
+ COHERE_API_KEY=...
114
+ GITHUB_USERNAME=neural-arun
115
+ GITHUB_TOKEN=github_pat_...
116
+ ```
117
+
118
+ ### 2. Run Backend Server (FastAPI on Port 8000)
119
+ ```bash
120
+ source .venv/bin/activate
121
+ python3 -m uvicorn backend.app.main:app --reload --port 8000
122
+ ```
123
+
124
+ ### 3. Run Frontend Dev Server (Next.js on Port 3000)
125
+ ```bash
126
+ cd frontend
127
+ npm run dev
128
+ ```
129
+
130
+ ### 4. Run Automated Evaluation Test Suite
131
+ ```bash
132
+ python3 scripts/evaluate.py
133
+ ```
134
+
135
+ ---
136
+
137
+ ## πŸ§ͺ Verification & Automated Testing
138
+
139
+ Execute the complete backend unit test suite:
140
+ ```bash
141
+ python3 -m unittest discover -s tests
142
+ ```
143
+ *Expected Output:* `Ran 9 tests in 0.028s OK`
144
+
145
+ ---
146
+
147
+ ## πŸ“œ License & Copyright
148
+
149
+ Β© 2026 **Arun Yadav** ([neural.arun.dev@gmail.com](mailto:neural.arun.dev@gmail.com)). All rights reserved.
aruncore_master_blueprint.md ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ›οΈ ArunCore β€” Master Enterprise AI System Blueprint & Architecture Specification (v1.0 Frozen)
2
+
3
+ > **STATUS**: ❄️ **ARCHITECTURE v1.0 FROZEN** (Official Release Specification)
4
+
5
+ ## πŸ“‹ Executive Overview & System Philosophy
6
+
7
+ **ArunCore** is a production-grade, stateful, agentic portfolio and multi-tenant enterprise AI engine platform created by **Arun Yadav** (AI Systems Architect specializing in Healthcare & Education).
8
+
9
+ **ArunCore** operates on a **Domain-Generic Engine Architecture** with **100% Preservation of Arun's Existing Next.js Frontend UI** and **Zero Loss of Existing Capabilities**. The **AI Agent stays in control 100% of the time**. Instead of complex live web page human takeovers, **ArunCore** relies on a high-precision **Hybrid RAG Engine** and an **Active Learning & Direct Messaging Loop (Telegram / WhatsApp)**:
10
+ - **Hybrid RAG Retrieval**: Combines dense vector search (ChromaDB), sparse keyword search (BM25), and Cohere/LLM reranking to deliver hyper-accurate, context-aware answers per tenant.
11
+ - **Active Learning Loop**: When a visitor asks an unknown, personal, weird, or low-confidence question, the AI Twin responds politely and alerts the owner on Telegram/WhatsApp with a 1-Click reply link.
12
+ - **Real-Time Vector Ingestion**: The owner replies directly in Telegram/WhatsApp, automatically ingesting the answer into ChromaDB in real time to train the AI Twin instantly for all future visitors!
13
+ - **3-Way Live Chat Presence**: When the owner joins a live session via Telegram, the AI system prompt dynamically injects a 3-way chat notice acknowledging the human instructor's live presence alongside the AI assistant.
14
+
15
+ ### The Business & Product Model:
16
+ 1. **Ultra-Simple Demo Mode (`?tutor=ed_donner` / `?client=ed_donner`)**: Instant data-driven client demos for 2-minute sales pitch videos to potential buyers. Passing `?tutor=ed_donner` in the URL fetches `/api/v1/config?tutor=ed_donner` (or `/config?tutor=ed_donner`), which dynamically updates the hero card title, role subtitle, avatar, welcome text, suggested questions, and AI system prompt on Arun's existing stunning UI!
17
+ 2. **Paid Client Onboarding ($300 – $1,000+ Tier)**: Rapid 5-minute onboarding of custom AI twins for tutors, instructors, doctors, lawyers, or consultants, with zero code duplication, zero backend `if client == ...` statements, zero frontend UI changes, and 100% isolated tenant data in `tenants/`.
18
+
19
+ ---
20
+
21
+ ## 🎯 The 11 Senior Systems Architect Principles
22
+
23
+ ```
24
+ πŸ₯‡ 1. Preserve Current Frontend πŸ₯ˆ 2. Zero Functionality Loss πŸ₯‰ 3. Production Hybrid RAG πŸ… 4. Ultra-Simple Demos
25
+ (Keep page.tsx, ChatPanel, (Keep RAG, Telegram alerts, (ChromaDB + BM25 + Cohere (?tutor=ed_donner dynamically
26
+ ProjectsView & Manifesto) TTS, 3-way live chat notice) dense/sparse hybrid engine) updates hero card & prompt)
27
+
28
+ πŸ… 5. Explicit tenants/ Storage πŸ… 6. Split Targeted Config πŸ… 7. Zero Client Ifs πŸ… 8. Config-Driven Tools
29
+ (PDFs, docs & configs in (brand, agent, chat, voice, (100% TenantService (enabled_tools array in
30
+ tenants/ outside Git) seo, social sub-configs) dynamic resolution) agent.json controls tools)
31
+
32
+ πŸ… 9. Separate Tenant Assets πŸ… 10. Decoupled Backend πŸ… 11. Direct Active Learning
33
+ (tenants/<id>/assets/ logos (PromptBuilder, ToolExec, (Owner replies via Telegram,
34
+ & avatars isolated) MemoryManager, AgentRunner) auto-ingesting into RAG DB)
35
+ ```
36
+
37
+ 1. **Preserve Current Frontend 100%**: Absolutely DO NOT replace or alter the existing Next.js frontend UI (`frontend/app/page.tsx`, `frontend/components/ChatPanel.tsx`, `ProjectsView.tsx`, `ManifestoView.tsx`, `Header.tsx`). The existing luxury UI, design tokens, glowing accent borders, light/dark themes, hero assistant card, and tab views stay 100% intact!
38
+ 2. **Zero Functionality Loss (Keep & Enhance)**: DO NOT remove a single working feature from the current app (Hybrid RAG, Telegram active learning alerts, 3-way live human chat presence, TTS neural voice, admin mode, session history). The goal is to keep 100% of existing functionality and upgrade it to production grade!
39
+ 3. **Production-Grade Hybrid RAG Engine**: Maintain and enhance the hybrid RAG architecture (dense ChromaDB vector search + sparse BM25 keyword search + Cohere/LLM reranking) for ultra-fast, high-precision knowledge retrieval per tenant.
40
+ 4. **Ultra-Simple Demo System**: Demo mode operates by passing a URL query parameter (`?tutor=ed_donner` or `?client=ed_donner`). The frontend fetches metadata from `/api/v1/config?tutor=ed_donner` (or `/config?tutor=ed_donner`), which dynamically populates the existing hero card title, role subtitle, avatar, welcome text, suggested questions, and AI system prompt. Zero complex slot engines required!
41
+ 5. **Explicit `tenants/` Storage**: All raw PDFs, markdown files, avatars, logos, and vector databases live in `./tenants/` outside the Git code repo. Keeps `git clone` lightweight (~10MB), enables fast deployments, self-documents tenant data isolation, and allows seamless S3/Cloudflare R2 sync.
42
+ 6. **Split Targeted Config**: Instead of one monolithic 238-key `config.json`, tenant configuration is divided into targeted JSON files (`brand.json`, `agent.json`, `chat.json`, `voice.json`, `seo.json`, `social.json`). Easy debugging, zero merge conflicts, Pydantic validated.
43
+ 7. **ZERO Backend Client Logic**: No `if client == "ed":` statements anywhere in Python code! All tenant logic is resolved dynamically by `TenantService`.
44
+ 8. **Config-Driven Tool Registry**: Client tool activation is controlled entirely via `enabled_tools` in `agent.json` (`["search_courses", "book_calendar", "faq_lookup"]`). `ToolExecutor` dynamically registers only enabled tools into the LLM execution loopβ€”zero Python code edits required!
45
+ 9. **Separation of Tenant Assets & Engine**: Static brand assets (`tenants/<id>/assets/avatars/`, `tenants/<id>/assets/logos/`) are kept separate from code logic.
46
+ 10. **Decoupled Single-Responsibility Backend Services**: `AgentService` is split into clean micro-services: `PromptBuilder`, `MemoryManager`, `ToolExecutor`, `AgentRunner`, `RAGService`, `NotificationService`, `ActiveLearningService`.
47
+ 11. **Interface Abstractions**: Abstract base classes for persistence and notification providers (`VectorStore`, `StateStore`, `NotificationProvider`). Swapping ChromaDB to Qdrant, Redis to Memory, or Telegram to WhatsApp takes 1 line of config!
48
+
49
+ ---
50
+
51
+ ## πŸ—οΈ End-to-End System Architecture
52
+
53
+ ```mermaid
54
+ graph TD
55
+ subgraph Client & UI Layer (frontend/ - 100% Preserved UI)
56
+ ArunVisitor["Arun Portfolio Visitor<br/>(www.neuralarun.in)"]
57
+ DemoVisitor["Demo / Client Visitor<br/>(?tutor=ed_donner)"]
58
+ end
59
+
60
+ subgraph API Gateway Layer (backend/app/api/v1/)
61
+ APIRouter["API Router Aggregator (router.py)"]
62
+ ChatAPI["/api/v1/chat"]
63
+ ConfigAPI["/api/v1/config"]
64
+ VoiceAPI["/api/v1/voice"]
65
+ WebhookAPI["/api/v1/webhook (Telegram / WhatsApp)"]
66
+ end
67
+
68
+ subgraph Business Logic Services (backend/app/services/)
69
+ TenantService["Tenant & Config Service"]
70
+ PromptBuilder["Prompt Builder Service"]
71
+ MemoryManager["Rolling Memory Manager (MemoryManager =<br/>RollingMemory alias)"]
72
+ ToolExecutor["Dynamic Config-Driven Tool Executor"]
73
+ AgentRunner["Agent Runner Engine (100% Agent Control)"]
74
+ KnowledgeService["Knowledge Retrieval Service (GitHub +<br/>READMEs + LinkedIn + static + Q&A)"]
75
+ RAGService["Hybrid RAG Coordinator (ChromaDB + BM25)"]
76
+ NotificationService["Telegram / Alert Dispatcher + Background Queue"]
77
+ SessionService["Thread-Safe Session & Memory Store"]
78
+ AuthService["Admin Token Auth (3-Way Live Takeover)"]
79
+ BackgroundQueue["Shared Background Task Queue"]
80
+ ActiveLearningService["Active Learning (owner-answer ingestion)"]
81
+ end
82
+
83
+ subgraph Interface Adapters (backend/app/db/ & core/)
84
+ VectorStore["VectorStore Interface<br/>(ChromaDB + BM25 + Cohere)"]
85
+ StateStore["StateStore Interface<br/>(Redis / Thread-Safe Memory)"]
86
+ NotificationProvider["NotificationProvider Interface<br/>(Telegram / WhatsApp API)"]
87
+ CompositionRoot["core/agent.py (thin composition<br/>root: init_agent + facade re-exports)"]
88
+ HTTPLayer["core/api.py (thin FastAPI wiring only)"]
89
+ end
90
+
91
+ subgraph Tenant Storage Layer (tenants/)
92
+ TenantConfigs["tenants/<id>/config/<br/>(brand, agent, chat, voice, seo, social)"]
93
+ TenantAssets["tenants/<id>/assets/<br/>(avatars, logos, graphics)"]
94
+ TenantKnowledge["tenants/<id>/knowledge/<br/>(markdown, PDFs, raw data, active_learning.json)"]
95
+ end
96
+
97
+ ArunVisitor --> ConfigAPI
98
+ ArunVisitor --> ChatAPI
99
+ DemoVisitor --> ConfigAPI
100
+ DemoVisitor --> ChatAPI
101
+
102
+ APIRouter --> ChatAPI
103
+ APIRouter --> ConfigAPI
104
+ APIRouter --> VoiceAPI
105
+ APIRouter --> WebhookAPI
106
+
107
+ ChatAPI --> AgentRunner
108
+ ConfigAPI --> TenantService
109
+ WebhookAPI --> ActiveLearningService
110
+
111
+ AgentRunner --> PromptBuilder
112
+ AgentRunner --> MemoryManager
113
+ AgentRunner --> ToolExecutor
114
+ AgentRunner --> KnowledgeService
115
+ AgentRunner --> RAGService
116
+ AgentRunner --> NotificationService
117
+ AgentRunner --> SessionService
118
+ AgentRunner --> CompositionRoot
119
+
120
+ ToolExecutor --> KnowledgeService
121
+ ToolExecutor --> NotificationService
122
+
123
+ NotificationService --> BackgroundQueue
124
+ NotificationService --> AuthService
125
+ HTTPLayer --> AgentRunner
126
+ HTTPLayer --> SessionService
127
+ HTTPLayer --> AuthService
128
+ HTTPLayer --> NotificationService
129
+
130
+ TenantService --> TenantConfigs
131
+ ToolExecutor --> TenantConfigs
132
+ RAGService --> KnowledgeService
133
+ KnowledgeService --> VectorStore
134
+ VectorStore --> TenantKnowledge
135
+ ActiveLearningService --> VectorStore
136
+ ActiveLearningService --> RAGService
137
+ NotificationService --> NotificationProvider
138
+ ```
139
+
140
+ ---
141
+
142
+ ## πŸ“ Repository Directory & Tenant Storage Structure
143
+
144
+ ```
145
+ aruncore/
146
+ β”œβ”€β”€ backend/ # βš™οΈ FASTAPI BACKEND SERVICE
147
+ β”‚ β”œβ”€β”€ app/
148
+ β”‚ β”‚ β”œβ”€β”€ api/ # 🌐 VERSIONED API ROUTERS (v1)
149
+ β”‚ β”‚ β”‚ β”œβ”€β”€ v1/
150
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ router.py # Master v1 API Router Aggregator
151
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ config.py # /api/v1/config multi-tenant metadata resolution
152
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ webhook.py # Telegram & WhatsApp reply webhook (Active Learning)
153
+ β”‚ β”‚ β”‚ β”‚ └── voice.py # /api/v1/voice/tts neural TTS audio
154
+ β”‚ β”‚ β”‚
155
+ β”‚ β”‚ β”œβ”€β”€ schemas/ # πŸ“‹ PYDANTIC VALIDATION SCHEMAS
156
+ β”‚ β”‚ β”‚ β”œβ”€β”€ tenant.py # Sub-config schemas (Brand, Agent, Chat, Voice, SEO, Social)
157
+ β”‚ β”‚ β”‚ β”œβ”€β”€ chat.py # ChatRequest, ChatHistoryResponse, NDJSONStreamChunk schemas
158
+ β”‚ β”‚ β”‚ β”œβ”€β”€ webhook.py # Telegram/WhatsApp incoming reply webhook schema
159
+ β”‚ β”‚ β”‚ └── voice.py # TTSRequest schema
160
+ β”‚ β”‚ β”‚
161
+ β”‚ β”‚ β”œβ”€β”€ services/ # 🧠 SINGLE-RESPONSIBILITY SERVICES (DECOUPLED)
162
+ β”‚ β”‚ β”‚ β”œβ”€β”€ background.py # Shared background task queue + worker thread
163
+ β”‚ β”‚ β”‚ β”œβ”€β”€ tenant_service.py # Dynamic tenant + legacy demos config resolver
164
+ β”‚ β”‚ β”‚ β”œβ”€β”€ prompt_builder.py # System prompt, static context & avatar assembly
165
+ β”‚ β”‚ β”‚ β”œβ”€β”€ memory_manager.py # RollingMemory summary-compression + MemoryManager alias
166
+ β”‚ β”‚ β”‚ β”œβ”€β”€ tool_executor.py # Real tool registry (search / github / notify) + placeholders
167
+ β”‚ β”‚ β”‚ β”œβ”€β”€ agent_runner.py # Streaming + sync agent loop (7-iters, 3-way live notice)
168
+ β”‚ β”‚ β”‚ β”œβ”€β”€ knowledge_service.py # Reads/writes ALL knowledge data (GitHub, LinkedIn, static, Q&A)
169
+ β”‚ β”‚ β”‚ β”œβ”€β”€ rag_service.py # Hybrid RAG coordinator + active-learning persistence
170
+ β”‚ β”‚ β”‚ β”œβ”€β”€ notification_service.py # Telegram send/alert/queue/logging + alert dedup
171
+ β”‚ β”‚ β”‚ β”œβ”€β”€ auth_service.py # Admin token generation + verification
172
+ β”‚ β”‚ β”‚ β”œβ”€β”€ session_store.py # Thread-safe session / human-control / memory store
173
+ β”‚ β”‚ β”‚ β”œβ”€β”€ active_learning_service.py # owner-reply -> RAG ingestion
174
+ β”‚ β”‚ β”‚ └── voice_service.py # Speech synthesis (TTS)
175
+ β”‚ β”‚ β”‚
176
+ β”‚ β”‚ β”œβ”€β”€ db/ # πŸ”Œ INTERFACE ADAPTERS
177
+ β”‚ β”‚ β”‚ β”œβ”€β”€ interfaces.py # Abstract Base Classes (VectorStore, StateStore, NotificationProvider)
178
+ β”‚ β”‚ β”‚ └── state_store.py # Thread-Safe In-Memory StateStore Implementation
179
+ β”‚ β”‚ β”‚
180
+ β”‚ β”‚ β”œβ”€β”€ core/ # πŸ”§ COMPOSITION ROOT, HTTP LAYER & CHANNELS
181
+ β”‚ β”‚ β”‚ β”œβ”€β”€ agent.py # init_agent factory + IPv4 patch + backward-compat facade
182
+ β”‚ β”‚ β”‚ β”œβ”€β”€ api.py # Thin FastAPI wiring (/chat, /config, /tts, admin, health)
183
+ β”‚ β”‚ β”‚ β”œβ”€β”€ bot.py # Telegram bot (uses shared AgentRunner.sync_reply)
184
+ β”‚ β”‚ β”‚ └── ingest.py # ChromaDB knowledge ingestion (data/ -> db/)
185
+ β”‚ β”‚ β”‚
186
+ β”‚ β”‚ └── main.py # FastAPI entrypoint (mounts v1 routers)
187
+ β”‚ β”‚
188
+ β”‚ β”œβ”€β”€ pyproject.toml
189
+ β”‚ └── Dockerfile
190
+ β”‚
191
+ β”œβ”€β”€ frontend/ # 🎨 NEXT.JS 16 FRONTEND WEB APP (100% PRESERVED)
192
+ β”‚ β”œβ”€β”€ app/
193
+ β”‚ β”‚ β”œβ”€β”€ page.tsx # Main App Router (Arun's Portfolio + Dynamic Tenant Metadata Loader)
194
+ β”‚ β”‚ β”œβ”€β”€ layout.tsx # Global layout provider
195
+ β”‚ β”‚ └── globals.css # Global design tokens & CSS root variables
196
+ β”‚ β”‚
197
+ β”‚ β”œβ”€β”€ components/
198
+ β”‚ β”‚ β”œβ”€β”€ ChatPanel.tsx # Primary Chat Panel & Hero Assistant Card (100% Preserved)
199
+ β”‚ β”‚ β”œβ”€β”€ ProjectsView.tsx # Projects Portfolio View (100% Preserved)
200
+ β”‚ β”‚ β”œβ”€β”€ ManifestoView.tsx # Manifesto View (100% Preserved)
201
+ β”‚ β”‚ β”œβ”€β”€ Header.tsx # Header bar & navigation (100% Preserved)
202
+ β”‚ β”‚ └── Sidebar.tsx # Sidebar navigation (100% Preserved)
203
+ β”‚ β”‚
204
+ β”‚ β”œβ”€β”€ hooks/ # Custom React Hooks
205
+ β”‚ β”œβ”€β”€ lib/ # Types & API helpers
206
+ β”‚ β”œβ”€β”€ package.json
207
+ β”‚ └── tsconfig.json
208
+ β”‚
209
+ β”œβ”€β”€ tenants/ # πŸ“¦ EXPLICIT TENANT DATA (OUTSIDE GIT REPO / S3 BUCKET)
210
+ β”‚ β”œβ”€β”€ ed_donner/ # Ed Donner Tenant Package
211
+ β”‚ β”‚ β”œβ”€β”€ config/ # πŸ“„ SPLIT CONFIG FILES
212
+ β”‚ β”‚ β”‚ β”œβ”€β”€ brand.json # Hero title, subtitle, colors, avatar path
213
+ β”‚ β”‚ β”‚ β”œβ”€β”€ agent.json # System prompt, guardrails, enabled_tools array
214
+ β”‚ β”‚ β”‚ β”œβ”€β”€ chat.json # Quick questions, welcome message
215
+ β”‚ β”‚ β”‚ β”œβ”€β”€ voice.json # TTS alloy voice specs
216
+ β”‚ β”‚ β”‚ β”œβ”€β”€ seo.json # Meta title & description
217
+ β”‚ β”‚ β”‚ └── social.json # Udemy, LinkedIn, X, website links
218
+ β”‚ β”‚ β”œβ”€β”€ assets/ # πŸ–ΌοΈ STATIC ASSETS (avatars, logos, graphics)
219
+ β”‚ β”‚ β”‚ β”œβ”€β”€ avatar.png
220
+ β”‚ β”‚ β”‚ └── logo.png
221
+ β”‚ β”‚ └── knowledge/ # πŸ“š RAW KNOWLEDGE (markdown, PDFs, active_learning.json)
222
+ β”‚ β”‚ β”œβ”€β”€ courses.md
223
+ β”‚ β”‚ └── active_learning.json
224
+ β”‚ β”‚
225
+ β”‚ β”œβ”€β”€ tenant_starter/ # Quick starter template for instant 1-min onboarding
226
+ β”‚ └── vector_db/ # Persistent ChromaDB sqlite & vector indexes
227
+ β”‚
228
+ β”œβ”€β”€ data/ # πŸ“Ή DEMO DATA & GLOBAL PROFILES
229
+ β”‚ └── static/ # Arun's static profile & rules of engagement
230
+ β”‚
231
+ β”œβ”€β”€ docs/ # Operational SOPs & Architecture Playbooks
232
+ β”œβ”€β”€ scripts/ # Vector ingestion (`ingest_knowledge.py`), evaluation scripts
233
+ β”œβ”€β”€ tests/ # Automated unit & integration test suites
234
+ β”œβ”€β”€ docker-compose.yml # Multi-container orchestration (Backend + Redis)
235
+ β”œβ”€β”€ Makefile # Developer workflow commands
236
+ └── README.md # Master Repository Overview
237
+ ```
238
+
239
+ ---
240
+
241
+ ## 🎯 Final Master Checklist
242
+
243
+ - [x] **100% Frontend Preservation**: Existing Next.js frontend UI (`page.tsx`, `ChatPanel.tsx`, `ProjectsView.tsx`, `ManifestoView.tsx`, `Header.tsx`) is preserved 100% with zero layout changes!
244
+ - [x] **Zero Functionality Loss**: 100% of existing features (Hybrid RAG, Telegram active learning, TTS, 3-way live human chat notice, admin mode) are retained and upgraded.
245
+ - [x] **Production-Grade Hybrid RAG Engine**: ChromaDB dense vector + BM25 sparse keyword search + Cohere/LLM reranking.
246
+ - [x] **Ultra-Simple Demo Mode**: Query param `?tutor=ed_donner` dynamically populates hero card title, role subtitle, avatar, welcome text, suggested questions, and AI system prompt.
247
+ - [x] **100% AI Agent Control**: Web chat is always handled by the AI Twin. Zero complex live web page human takeovers needed.
248
+ - [x] **Direct Active Learning Loop**: Owner replies directly in Telegram or WhatsApp to train their AI Twin in real time.
249
+ - [x] **Split Targeted Config**: 6 targeted JSON files (`brand`, `agent`, `chat`, `voice`, `seo`, `social`).
250
+ - [x] **Explicit `tenants/` Directory**: External tenant data (`tenants/<id>/config`, `assets`, `knowledge`) isolated outside Git code repo.
251
+ - [x] **ZERO Client `if` Statements**: 100% dynamic `TenantService` resolution in backend.
252
+ - [x] **Config-Driven Tool Registry**: `agent.json` controls `enabled_tools` array.
253
+ - [x] **Separation of Tenant Assets**: `tenants/<id>/assets/` separate from code.
254
+ - [x] **Single-Responsibility Services**: Decoupled `PromptBuilder`, `MemoryManager`, `ToolExecutor`, `AgentRunner`.
255
+ - [x] **Interface Abstractions**: Abstract classes for `VectorStore`, `StateStore`, and `NotificationProvider`.
256
+ - [x] **Domain-Generic Backend**: Backend engine only understands `Tenant`, `Conversation`, `Knowledge`, `Tool`, `Active Learning`, `Voice`.
257
+ - [x] **Self-Documenting Naming**: Explicit module and directory names throughout.
backend/README.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # βš™οΈ ArunCore Backend Architecture (`backend/`)
2
+
3
+ > **Architecture Spec:** Decoupled Single-Responsibility Services
4
+ > **Framework:** FastAPI / Uvicorn
5
+ > **LLM Engine:** `gpt-4.1-nano` (via LangChain `ChatOpenAI`)
6
+ > **RAG Engine:** ChromaDB + BM25 + Cohere English V3 Reranker
7
+
8
+ ---
9
+
10
+ ## πŸ“ Backend Directory Map
11
+
12
+ ```text
13
+ backend/app/
14
+ β”œβ”€β”€ api/v1/ # REST API Endpoints & Webhooks
15
+ β”‚ β”œβ”€β”€ config.py # GET /api/v1/config (238-key enterprise JSON resolver)
16
+ β”‚ β”œβ”€β”€ router.py # Main API Router
17
+ β”‚ β”œβ”€β”€ voice.py # POST /api/v1/voice/speak & audio STT endpoint
18
+ β”‚ └── webhook.py # POST /api/v1/webhook (Telegram live takeover webhook)
19
+ β”œβ”€β”€ core/ # Agent Orchestration Core
20
+ β”‚ β”œβ”€β”€ agent.py # LLM init, search_arun_knowledge, get_github_live_data, notify_arun
21
+ β”‚ β”œβ”€β”€ api.py # Legacy API handler compatibility layer
22
+ β”‚ β”œβ”€β”€ bot.py # Telegram bot delivery helpers
23
+ β”‚ β”œβ”€β”€ evaluate.py # Core evaluation functions
24
+ β”‚ └── ingest.py # Local vector store ingestion module
25
+ β”œβ”€β”€ schemas/ # Pydantic Schemas & Data Contracts
26
+ β”‚ β”œβ”€β”€ chat.py # ChatRequest, ChatResponse
27
+ β”‚ β”œβ”€β”€ tenant.py # TenantConfig schema
28
+ β”‚ β”œβ”€β”€ voice.py # VoiceSpeakRequest, VoiceSpeakResponse
29
+ β”‚ └── webhook.py # TelegramWebhookPayload
30
+ β”œβ”€β”€ services/ # Decoupled Business Logic Services
31
+ β”‚ β”œβ”€β”€ active_learning_service.py # Unknown question logger & memory saver
32
+ β”‚ β”œβ”€β”€ memory_manager.py # RollingMemory summary compression engine
33
+ β”‚ β”œβ”€β”€ notification_service.py # Telegram alert queue & delivery service
34
+ β”‚ β”œβ”€β”€ prompt_builder.py # Dynamic System Prompt assembler
35
+ β”‚ β”œβ”€β”€ rag_service.py # Hybrid Vector Search + BM25 + Reranker pipeline
36
+ β”‚ β”œβ”€β”€ tenant_service.py # Dynamic JSON config loader (Zero backend `if` checks)
37
+ β”‚ β”œβ”€β”€ tool_executor.py # Config-driven tool binder & execution loop
38
+ β”‚ └── voice_service.py # OpenAI tts-1 audio synthesizer
39
+ └── main.py # FastAPI Application Entrypoint
40
+ ```
41
+
42
+ ---
43
+
44
+ ## πŸ”Œ Primary API Endpoints
45
+
46
+ ### 1. `GET /api/v1/config?tutor=<tutor_id>`
47
+ - Resolves tenant configuration dynamically from `demos/<tutor_id>_enterprise_dictionary.json`.
48
+ - Returns full 238-key JSON payload including `theme_design_system`, `frontend_ui_dictionary`, `courses`, and `custom_system_prompt`.
49
+
50
+ ### 2. `POST /chat` / `POST /api/v1/chat`
51
+ - Accepts user messages and executes the 7-iteration ReAct tool loop (`search_arun_knowledge`, `get_github_live_data`, `notify_arun`).
52
+ - Streams or returns the final response along with tool execution traces.
53
+
54
+ ### 3. `POST /api/v1/voice/speak`
55
+ - Synthesizes text into natural studio neural speech audio using OpenAI `tts-1` (`alloy` voice).
56
+
57
+ ---
58
+
59
+ ## πŸ§ͺ Testing Backend Services
60
+
61
+ Run all backend test cases:
62
+ ```bash
63
+ python3 -m unittest discover -s tests
64
+ ```
backend/app/api/v1/config.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from fastapi import APIRouter
3
+ from backend.app.services.tenant_service import tenant_service
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/config")
9
+ @router.get("/api/v1/config")
10
+ async def get_tutor_config_endpoint(tutor: Optional[str] = None):
11
+ """Dynamic multi-tenant metadata resolution endpoint powering 1-click demos (?tutor=ed_donner)."""
12
+ full_cfg = tenant_service.load_tenant_config(tutor)
13
+ return full_cfg.to_legacy_dict()
backend/app/api/v1/router.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from backend.app.api.v1.config import router as config_router
3
+ from backend.app.api.v1.voice import router as voice_router
4
+ from backend.app.api.v1.webhook import router as webhook_router
5
+
6
+ api_v1_router = APIRouter()
7
+ api_v1_router.include_router(config_router)
8
+ api_v1_router.include_router(voice_router)
9
+ api_v1_router.include_router(webhook_router)
backend/app/api/v1/voice.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ from fastapi import APIRouter, HTTPException
3
+ from fastapi.responses import StreamingResponse
4
+ from backend.app.schemas.voice import TTSRequest
5
+ from backend.app.services.voice_service import VoiceService
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.post("/tts")
11
+ @router.post("/api/v1/voice/tts")
12
+ async def tts_endpoint(req: TTSRequest):
13
+ if not req.text.strip():
14
+ raise HTTPException(status_code=400, detail="Text snippet cannot be empty.")
15
+
16
+ try:
17
+ audio_bytes = VoiceService.generate_tts_audio(req.text, voice=req.voice or "alloy")
18
+ return StreamingResponse(io.BytesIO(audio_bytes), media_type="audio/mpeg")
19
+ except Exception as e:
20
+ raise HTTPException(status_code=500, detail=str(e))
backend/app/api/v1/webhook.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from backend.app.schemas.webhook import ActiveLearningWebhookPayload
3
+ from backend.app.services.active_learning_service import ActiveLearningService
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.post("/webhook/telegram")
9
+ @router.post("/api/v1/webhook")
10
+ async def telegram_webhook_endpoint(payload: ActiveLearningWebhookPayload):
11
+ try:
12
+ active_learning = ActiveLearningService(tenant_id=payload.tutor_id or "arun")
13
+ success = active_learning.process_incoming_owner_reply(
14
+ session_id=payload.session_id,
15
+ question=payload.user_question,
16
+ answer=payload.owner_answer,
17
+ )
18
+ return {"status": "success" if success else "failed", "session_id": payload.session_id}
19
+ except Exception as e:
20
+ raise HTTPException(status_code=500, detail=str(e))
backend/app/core/README.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # βš™οΈ Core Engine (`/core/`)
2
+
3
+ This folder contains the **Python backend engine** for ArunCore. It handles the FastAPI API server, AI reasoning loop (`gpt-4.1-nano`), vector database search, OpenAI studio voice synthesis, real-time GitHub data fetching, **100% automated Telegram notifications**, a **1-Click Magic Link 3-Way Real Human Takeover Engine**, a **Deterministic Human Control State Machine**, and the **Active Learning Memory Loop**.
4
+
5
+ ---
6
+
7
+ ## πŸ“ Files Overview & Descriptions
8
+
9
+ ### 1. 🌐 `core/api.py` β€” FastAPI Web Server & 3-Way Live Chat Engine
10
+ - Runs the async HTTP server (`port 8000` locally, `port 7860` on Hugging Face Spaces).
11
+ - **Key Endpoints**:
12
+ - `POST /chat`: Streams real-time AI responses token-by-token (NDJSON) with execution trace thoughts. Unconditionally queues automated Telegram chat alerts for EVERY visitor message.
13
+ - `GET /chat/history`: Returns full central session chat transcript (`SESSION_CHAT_STORE`) so both visitor and Real Arun see the exact same 3-way conversation history in real time.
14
+ - `POST /chat/human-message`: Receives live messages sent by Real Arun from the Admin Reply Bar or Telegram commands (`/answer`, `/release`). Automatically pairs Real Arun's answer with the visitor's question, appends to `data/raw/unknown_questions.json`, and triggers background ChromaDB vector DB re-ingestion!
15
+ - `GET /chat/verify-admin-token`: Validates secure HMAC admin tokens generated for 1-click Telegram magic join links.
16
+ - `POST /tts`: Converts AI text into HD neural studio voice using OpenAI `tts-1` (`alloy` voice).
17
+ - `GET /health`: Health check endpoint showing active sessions, Telegram log statuses, and system uptime.
18
+
19
+ ---
20
+
21
+ ### 2. 🧠 `core/agent.py` β€” AI Persona, Agentic Loop & Tools
22
+ - Configures the main LLM (`gpt-4.1-nano`), persona system prompt, and tool functions.
23
+ - **Dynamic 100% Language Matching**: Responds in 100% articulate English for English queries with zero Hindi/Hinglish slang leaks. Responds naturally in Hinglish/Hindi when the user speaks Hindi.
24
+ - **3-Way Human Presence Injection**: When Real Arun is active in a session, dynamically injects a system notice containing Real Arun's messages into the prompt context so the AI Twin recognizes Real Arun's presence and co-pilots seamlessly.
25
+ - **Tools**:
26
+ - `search_arun_knowledge`: Alias-aware project search (resolves `MedCoach` β†’ `med_coach/README.md`), always includes `public_profile.md` and checks `data/raw/unknown_questions.json` for Arun's verified human answers.
27
+ - `get_github_live_data`: Fetches real-time public repositories and recent commit activity from GitHub (`api.github.com/users/neural-arun/repos`).
28
+ - `notify_arun`: Sends instant Telegram alerts (`@ai_twin_alert_bot`) for leads or urgent queries.
29
+ - **1-Click Magic Link Generation**: `generate_admin_token(session_id)` & `verify_admin_token(session_id, token)` create tamper-proof magic links (`https://aruncore.vercel.app/?session_id=...&admin_token=...`).
30
+ - **Active Learning**: `save_unknown_question_answer(question, answer)` appends verified Q&A pairs to `data/raw/unknown_questions.json` and triggers background re-ingestion.
31
+
32
+ ---
33
+
34
+ ### 3. πŸ“š `core/ingest.py` β€” Vector Database Compiler
35
+ - Scans all markdown and JSON files inside `data/`, chunks them semantically, generates vector embeddings (`text-embedding-3-small`), and compiles into ChromaDB (`db/`).
36
+ - **Smart JSON Parsing**: Specially parses `data/raw/unknown_questions.json` β€” each Q&A pair becomes its own searchable vector chunk.
37
+
38
+ ---
39
+
40
+ ### 4. πŸ€– `core/bot.py` β€” Public Telegram Bot Service & Active Learning Handler
41
+ - Runs a standalone Telegram bot allowing visitors to interact with Arun's AI Assistant directly in Telegram.
42
+ - **Active Learning Reply Handler**: When Arun replies to an alert message in Telegram:
43
+ 1. Extracts original user question from alert message.
44
+ 2. Pairs with Arun's reply as verified answer.
45
+ 3. Saves to `data/raw/unknown_questions.json`.
46
+ 4. Triggers background ChromaDB re-ingestion.
47
+ 5. Sends Arun confirmation: *"βœ… Answer Saved & Ingested into AI Memory!"*
48
+
49
+ ---
50
+
51
+ ### 5. πŸ§ͺ `core/evaluate.py` β€” Evaluation & Benchmarking Engine
52
+ - Stress-tests benchmark evaluations against test sets to measure retrieval precision and generation quality.
backend/app/core/__init__.py ADDED
File without changes
backend/app/core/agent.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent composition root + backwards-compatible facade.
2
+
3
+ The heavy lifting previously in this file now lives in single-responsibility
4
+ services (notification, knowledge, memory, prompt, tools, tenant). This module
5
+ only: (1) applies the global IPv4-only DNS patch once, (2) exposes `init_agent`
6
+ as the small factory that wires an LLM + bound tools + chat prompt + memory,
7
+ and (3) re-exports every public symbol the API, Telegram bot, eval scripts,
8
+ and tests still import from `backend.app.core.agent`.
9
+ """
10
+ import os
11
+ import socket
12
+ from typing import Any, Dict, Optional, Tuple
13
+
14
+ # Force IPv4-only DNS resolution (Hugging Face Spaces / dual-stack hosts can
15
+ # stall IPv6 lookups). Applied once at import time, mirroring the legacy boot.
16
+ try:
17
+ _orig_getaddrinfo = socket.getaddrinfo
18
+
19
+ def _ipv4_only_getaddrinfo(*args, **kwargs):
20
+ res = _orig_getaddrinfo(*args, **kwargs)
21
+ ipv4_res = [r for r in res if r[0] == socket.AF_INET]
22
+ return ipv4_res or res
23
+
24
+ socket.getaddrinfo = _ipv4_only_getaddrinfo
25
+ except Exception:
26
+ pass
27
+
28
+ from dotenv import load_dotenv
29
+ from langchain_openai import ChatOpenAI
30
+
31
+ from backend.app.services.memory_manager import RollingMemory, MemoryManager
32
+ from backend.app.services.prompt_builder import PromptBuilder
33
+ from backend.app.services.tool_executor import (
34
+ ToolExecutor,
35
+ search_arun_knowledge,
36
+ get_github_live_data,
37
+ notify_arun,
38
+ )
39
+ from backend.app.services.tenant_service import tenant_service, TenantService
40
+ from backend.app.services.knowledge_service import knowledge_service
41
+ from backend.app.services.notification_service import (
42
+ queue_debug_event,
43
+ queue_maybe_notify_arun,
44
+ queue_chat_history_to_telegram,
45
+ queue_automated_chat_alert,
46
+ send_automated_chat_alert,
47
+ send_chat_history_to_telegram,
48
+ send_debug_event_to_telegram,
49
+ schedule_notify_arun,
50
+ TELEGRAM_DELIVERY_LOGS,
51
+ _deliver_notify_arun,
52
+ )
53
+ from backend.app.services.auth_service import generate_admin_token, verify_admin_token
54
+ from backend.app.services.agent_runner import AgentRunner, agent_runner, run_pre_escalation
55
+
56
+ load_dotenv()
57
+
58
+ __all__ = [
59
+ "init_agent",
60
+ "RollingMemory",
61
+ "MemoryManager",
62
+ "load_static_context",
63
+ "load_tutor_config",
64
+ "save_unknown_question_answer",
65
+ "queue_debug_event",
66
+ "queue_maybe_notify_arun",
67
+ "run_pre_escalation",
68
+ "queue_chat_history_to_telegram",
69
+ "queue_automated_chat_alert",
70
+ "send_automated_chat_alert",
71
+ "send_chat_history_to_telegram",
72
+ "send_debug_event_to_telegram",
73
+ "schedule_notify_arun",
74
+ "generate_admin_token",
75
+ "verify_admin_token",
76
+ "TELEGRAM_DELIVERY_LOGS",
77
+ "search_arun_knowledge",
78
+ "get_github_live_data",
79
+ "notify_arun",
80
+ "ToolExecutor",
81
+ "AgentRunner",
82
+ "agent_runner",
83
+ ]
84
+
85
+
86
+ def init_agent(
87
+ temperature: float = 0.4,
88
+ model_name: Optional[str] = None,
89
+ tutor_id: Optional[str] = None,
90
+ ):
91
+ """Build a ready-to-run ArunCore agent.
92
+
93
+ Returns the same 4-tuple as the legacy factory:
94
+ (main_llm, chat_prompt, memory, tools).
95
+ """
96
+ openai_key = os.getenv("OPENAI_API_KEY")
97
+ if not openai_key:
98
+ raise ValueError("OPENAI_API_KEY is not set in environment.")
99
+
100
+ resolved_model = model_name or os.getenv("OPENAI_MODEL", "gpt-4.1-nano")
101
+
102
+ tools = ToolExecutor.get_enabled_tools(ToolExecutor.DEFAULT_TOOLS)
103
+ main_llm = ChatOpenAI(
104
+ temperature=temperature,
105
+ model=resolved_model,
106
+ api_key=openai_key,
107
+ ).bind_tools(tools)
108
+
109
+ system_prompt = PromptBuilder().build_system_prompt(tutor_id=tutor_id)
110
+ prompt = PromptBuilder.build_chat_prompt(system_prompt)
111
+
112
+ summary_llm = ChatOpenAI(
113
+ temperature=0.0,
114
+ model="gpt-4o-mini",
115
+ api_key=openai_key,
116
+ )
117
+ memory = RollingMemory(summary_llm=summary_llm)
118
+
119
+ return main_llm, prompt, memory, tools
120
+
121
+
122
+ def load_tutor_config(tutor_id: Optional[str]) -> Optional[Dict[str, Any]]:
123
+ """Legacy demos-dictionary loader (kept for external consumers)."""
124
+ return tenant_service.load_legacy_tutor_config(tutor_id)
125
+
126
+
127
+ def save_unknown_question_answer(question: str, answer: str) -> str:
128
+ """Persists a verified Q&A pair into the active-learning store."""
129
+ return knowledge_service.save_verified_answer(question, answer)
130
+
131
+
132
+ def load_static_context():
133
+ """Canonical 5-tuple static context reader (system, guardrails, handoff, profile, rules)."""
134
+ from backend.app.services.prompt_builder import load_static_context as _load_5
135
+
136
+ return _load_5()
backend/app/core/api.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ArunCore FastAPI HTTP layer.
2
+
3
+ This file only wires HTTP: request validation, routing, CORS, and static file
4
+ serving. Every piece of business logic lives in single-responsibility services
5
+ and is delegated here (agent runner loops, session store, notification, voice,
6
+ tenant config, auth, knowledge persistence).
7
+ """
8
+ import os
9
+ import json
10
+ import io
11
+ from typing import Optional
12
+
13
+ from dotenv import load_dotenv
14
+ from fastapi import FastAPI, HTTPException
15
+ from pydantic import BaseModel
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import StreamingResponse
18
+
19
+ from backend.app.core.agent import init_agent, verify_admin_token
20
+ from backend.app.services.session_store import session_store
21
+ from backend.app.services.agent_runner import agent_runner
22
+ from backend.app.services.knowledge_service import knowledge_service
23
+ from backend.app.services.tenant_service import tenant_service
24
+ from backend.app.services.notification_service import TELEGRAM_DELIVERY_LOGS
25
+
26
+ load_dotenv()
27
+
28
+ app = FastAPI(title="ArunCore API", description="Stateful Agentic Backend for Arun Yadav's Digital Twin.")
29
+
30
+ # Enable CORS for external frontends (Vercel, custom domains, local dev)
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=["*"],
34
+ allow_origin_regex=r"https://.*\.vercel\.app",
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ # Initialize the ArunCore engine once (mirrors legacy boot).
41
+ try:
42
+ print("Initializing ArunCore API Backend...")
43
+ _, _, _, tools = init_agent()
44
+ global_tool_map = {t.name: t for t in tools}
45
+ print("API Backend Initialized Successfully.")
46
+ except Exception as e:
47
+ print(f"Failed to initialize backend: {e}")
48
+ raise e
49
+
50
+
51
+ class ChatRequest(BaseModel):
52
+ session_id: str
53
+ message: str
54
+ tutor_id: Optional[str] = None
55
+
56
+
57
+ class TTSRequest(BaseModel):
58
+ text: str
59
+ voice: Optional[str] = "alloy"
60
+
61
+
62
+ class HumanMessageRequest(BaseModel):
63
+ session_id: str
64
+ admin_token: str
65
+ message: str
66
+
67
+
68
+ @app.get("/api/config")
69
+ @app.get("/config")
70
+ async def get_tutor_config_endpoint(tutor: Optional[str] = None):
71
+ cfg = tenant_service.load_legacy_tutor_config(tutor)
72
+ if not cfg:
73
+ return {"tutor_id": tutor or "arun", "config": None}
74
+ cfg["tutor_id"] = tutor or "arun"
75
+ return cfg
76
+
77
+
78
+ @app.post("/chat")
79
+ async def chat_endpoint(req: ChatRequest):
80
+ if not req.message.strip():
81
+ raise HTTPException(status_code=400, detail="Message cannot be empty.")
82
+
83
+ async def generate_response():
84
+ async for chunk in agent_runner.stream_chat(
85
+ session_id=req.session_id,
86
+ message=req.message,
87
+ tutor_id=req.tutor_id,
88
+ tool_map=global_tool_map,
89
+ ):
90
+ yield json.dumps(chunk) + "\n"
91
+
92
+ return StreamingResponse(generate_response(), media_type="application/x-ndjson")
93
+
94
+
95
+ @app.post("/tts")
96
+ async def tts_endpoint(req: TTSRequest):
97
+ from backend.app.services.voice_service import VoiceService
98
+
99
+ if not req.text.strip():
100
+ raise HTTPException(status_code=400, detail="Text cannot be empty.")
101
+
102
+ try:
103
+ audio_bytes = VoiceService.generate_tts_audio(req.text, voice=req.voice or "alloy")
104
+ return StreamingResponse(io.BytesIO(audio_bytes), media_type="audio/mpeg")
105
+ except Exception as e:
106
+ raise HTTPException(status_code=500, detail=str(e))
107
+
108
+
109
+ @app.get("/health")
110
+ def health_check():
111
+ alert_tok = os.getenv("TELEGRAM_ALERT_BOT_TOKEN", "")
112
+ bot_tok = os.getenv("TELEGRAM_BOT_TOKEN", "")
113
+ alert_cid = os.getenv("TELEGRAM_ALERT_CHAT_ID", "")
114
+ bot_cid = os.getenv("TELEGRAM_CHAT_ID")
115
+
116
+ return {
117
+ "status": "ok",
118
+ "active_sessions": session_store.liveness,
119
+ "telegram_alert_bot_preview": f"{alert_tok[:6]}...{alert_tok[-4:]}" if alert_tok else "MISSING",
120
+ "telegram_alert_chat_id": alert_cid or "MISSING",
121
+ "telegram_bot_preview": f"{bot_tok[:6]}...{bot_tok[-4:]}" if bot_tok else "MISSING",
122
+ "telegram_chat_id": bot_cid or "MISSING",
123
+ "telegram_logs": TELEGRAM_DELIVERY_LOGS[-10:],
124
+ }
125
+
126
+
127
+ @app.post("/chat/human-message")
128
+ async def post_human_message(req: HumanMessageRequest):
129
+ if not verify_admin_token(req.session_id, req.admin_token):
130
+ raise HTTPException(status_code=403, detail="Invalid admin token.")
131
+
132
+ clean_text = (req.message or "").strip()
133
+ if not clean_text:
134
+ raise HTTPException(status_code=400, detail="Message cannot be empty.")
135
+
136
+ lowered = clean_text.lower().strip()
137
+
138
+ # Command: /release or /resume -> Hand control back to AI Twin
139
+ if lowered in ("/release", "/resume"):
140
+ session_store.set_human_control(req.session_id, False)
141
+ rel_entry = session_store.record_message(
142
+ req.session_id, "human_arun",
143
+ "[Handed back auto-response control to AI Twin]",
144
+ name="Arun Yadav",
145
+ )
146
+ return {"status": "success", "entry": rel_entry, "human_control": False}
147
+
148
+ # Command: /answer -> Trigger AI Twin to answer reading the 3-way transcript
149
+ if lowered.startswith("/answer"):
150
+ session_store.set_human_control(req.session_id, True)
151
+ extra_prompt = clean_text[7:].strip()
152
+ cmd_entry = session_store.record_message(
153
+ req.session_id, "human_arun", f"/answer {extra_prompt}".strip(), name="Arun Yadav"
154
+ )
155
+ ai_reply = await agent_runner.trigger_ai_answer(req.session_id, extra_prompt)
156
+ return {"status": "success", "entry": cmd_entry, "ai_reply": ai_reply, "human_control": True}
157
+
158
+ # Real Arun's first human message activates Human Control Mode
159
+ session_store.set_human_control(req.session_id, True)
160
+ entry = session_store.record_message(req.session_id, "human_arun", clean_text, name="Arun Yadav")
161
+ session_store.append_human_message(req.session_id, entry)
162
+
163
+ last_user_msg = session_store.get_last_user_message(req.session_id)
164
+
165
+ if last_user_msg:
166
+ try:
167
+ knowledge_service.save_verified_answer(last_user_msg, clean_text)
168
+ print(f"[RAG AUTO-UPDATE] Saved Q&A pair to unknown_questions.json & triggered ChromaDB re-ingestion.")
169
+ except Exception as e:
170
+ print(f"[RAG AUTO-UPDATE ERROR] Failed to auto-ingest admin answer: {e}")
171
+
172
+ memory = session_store.get_or_create_memory(req.session_id)
173
+ memory.add_interaction(f"[REAL ARUN JOINED LIVE]: {clean_text}", "Acknowledged real Arun input.")
174
+
175
+ return {"status": "success", "entry": entry, "human_control": True}
176
+
177
+
178
+ @app.get("/chat/history")
179
+ async def get_chat_history(session_id: str):
180
+ msgs = session_store.get_history(session_id)
181
+ return {"session_id": session_id, "messages": msgs}
182
+
183
+
184
+ @app.get("/chat/human-messages")
185
+ async def get_human_messages(session_id: str):
186
+ msgs = session_store.get_human_messages(session_id)
187
+ return {"session_id": session_id, "messages": msgs}
188
+
189
+
190
+ @app.get("/chat/verify-admin-token")
191
+ async def verify_admin(session_id: str, admin_token: str):
192
+ valid = verify_admin_token(session_id, admin_token)
193
+ return {"valid": valid, "session_id": session_id}
194
+
195
+
196
+ # Mount static frontend export if built (for Hugging Face Spaces production deployment)
197
+ frontend_out = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend", "out")
198
+ if os.path.exists(frontend_out):
199
+ from fastapi.staticfiles import StaticFiles
200
+
201
+ app.mount("/", StaticFiles(directory=frontend_out, html=True), name="static_frontend")
202
+
203
+ if __name__ == "__main__":
204
+ import uvicorn
205
+
206
+ port = int(os.getenv("PORT", "8000"))
207
+ uvicorn.run("backend.app.main:app", host="0.0.0.0", port=port, reload=False)
backend/app/core/bot.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import re
4
+ from dotenv import load_dotenv
5
+ from langchain_openai import ChatOpenAI
6
+ from telegram import Update
7
+ from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
8
+
9
+ # Import the ArunCore engine (composition root + decoupled services)
10
+ from backend.app.core.agent import init_agent
11
+ from backend.app.services.memory_manager import RollingMemory
12
+ from backend.app.services.agent_runner import agent_runner
13
+ from backend.app.services.knowledge_service import knowledge_service
14
+
15
+ load_dotenv()
16
+
17
+ # === In-Memory Session Store (telegram chat_id -> RollingMemory) ===
18
+ sessions: dict[int, RollingMemory] = {}
19
+
20
+ # Initialize the engine once at startup
21
+ print("Initializing ArunCore Telegram Bot...")
22
+ main_llm, prompt, _, tools = init_agent()
23
+ tool_map = {t.name: t for t in tools}
24
+ print("Bot engine ready.")
25
+
26
+
27
+ def get_or_create_memory(chat_id: int) -> RollingMemory:
28
+ """Returns existing memory for this user, or creates a new one."""
29
+ if chat_id not in sessions:
30
+ summary_llm = ChatOpenAI(
31
+ temperature=0.0,
32
+ model="gpt-4o-mini",
33
+ api_key=os.getenv("OPENAI_API_KEY"),
34
+ )
35
+ sessions[chat_id] = RollingMemory(summary_llm=summary_llm)
36
+ return sessions[chat_id]
37
+
38
+
39
+ def run_agent(chat_id: int, user_message: str) -> str:
40
+ """Runs the full stateful agent loop via the shared AgentRunner."""
41
+ memory = get_or_create_memory(chat_id)
42
+ return agent_runner.sync_reply(
43
+ session_id=str(chat_id),
44
+ user_input=user_message,
45
+ llm=main_llm,
46
+ prompt=prompt,
47
+ memory=memory,
48
+ tool_map=tool_map,
49
+ user_metadata={"channel": "telegram", "chat_id": chat_id},
50
+ max_iterations=3,
51
+ )
52
+
53
+
54
+ # === Telegram Handlers ===
55
+
56
+ async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
57
+ welcome = (
58
+ "Hi! I'm *ArunCore*, the AI digital twin of *Arun Yadav*.\n\n"
59
+ "Ask me anything about his projects, skills, or background in AI engineering. "
60
+ "I'm here to give you the real picture."
61
+ )
62
+ await update.message.reply_text(welcome, parse_mode="Markdown")
63
+
64
+
65
+ def format_for_telegram(text: str) -> str:
66
+ """Converts LLM Markdown into Telegram-safe HTML."""
67
+ text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
68
+ text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text)
69
+ text = re.sub(r'^###?\s+(.+)$', r'\n<b>\1</b>', text, flags=re.MULTILINE)
70
+ text = re.sub(r'```(?:[a-zA-Z]+)?\n?(.*?)\n?```', r'<pre>\1</pre>', text, flags=re.DOTALL)
71
+ text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
72
+ text = re.sub(r'^[*-]\s+', 'β€’ ', text, flags=re.MULTILINE)
73
+
74
+ def link_repl(match):
75
+ label, url = match.groups()
76
+ return f'<a href="{url}">{label}</a>'
77
+
78
+ text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', link_repl, text)
79
+ return text.strip()
80
+
81
+
82
+ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
83
+ user_text = update.message.text
84
+ chat_id = update.effective_chat.id
85
+
86
+ # Check if this is a Telegram reply to an Alert message
87
+ if update.message.reply_to_message:
88
+ reply_to_text = update.message.reply_to_message.text or update.message.reply_to_message.caption or ""
89
+
90
+ extracted_question = ""
91
+ if "User Query / Details:" in reply_to_text:
92
+ parts = reply_to_text.split("User Query / Details:")
93
+ if len(parts) > 1:
94
+ extracted_question = parts[1].split("Category:")[0].split("Contact:")[0].split("Chat ID:")[0].strip()
95
+ elif "User Message:" in reply_to_text:
96
+ parts = reply_to_text.split("User Message:")
97
+ if len(parts) > 1:
98
+ extracted_question = parts[1].split("Category:")[0].split("Contact:")[0].split("Chat ID:")[0].strip()
99
+
100
+ if not extracted_question and len(reply_to_text) > 5:
101
+ extracted_question = reply_to_text.split("\n\n")[0].strip()
102
+
103
+ if extracted_question:
104
+ res = knowledge_service.save_verified_answer(extracted_question, user_text)
105
+ confirmation = (
106
+ f"<b>βœ… Answer Saved & Ingested into AI Memory!</b>\n\n"
107
+ f"<b>Question:</b> <code>{extracted_question}</code>\n"
108
+ f"<b>Your Verified Answer:</b>\n{user_text}\n\n"
109
+ f"<i>Result: {res}</i>"
110
+ )
111
+ await update.message.reply_text(confirmation, parse_mode="HTML")
112
+ return
113
+
114
+ await update.message.chat.send_action("typing")
115
+ reply = await asyncio.to_thread(run_agent, chat_id, user_text)
116
+
117
+ html_reply = format_for_telegram(reply)
118
+ try:
119
+ await update.message.reply_text(html_reply, parse_mode="HTML")
120
+ except Exception:
121
+ await update.message.reply_text(reply)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ token = os.getenv("TELEGRAM_PUBLIC_BOT_TOKEN")
126
+ if not token:
127
+ raise ValueError("TELEGRAM_PUBLIC_BOT_TOKEN not set in .env")
128
+
129
+ application = Application.builder().token(token).build()
130
+ application.add_handler(CommandHandler("start", start_handler))
131
+ application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler))
132
+
133
+ print("ArunCore Telegram Bot is running...")
134
+ application.run_polling()
backend/app/core/evaluate.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import time
4
+ from pathlib import Path
5
+ from dotenv import load_dotenv
6
+
7
+ # Import the agent logic from core.agent
8
+ from core.agent import init_agent, answer_query
9
+
10
+ # Load environment variables
11
+ load_dotenv()
12
+
13
+ BASE_DIR = Path(__file__).resolve().parent.parent
14
+ EVAL_SET_PATH = BASE_DIR / "data" / "test_set" / "eval_set.json"
15
+ REPORT_PATH = BASE_DIR / "data" / "test_set" / "evaluation_report.json"
16
+ DEBUG_DIR = BASE_DIR / "evaluation_debug"
17
+
18
+ def fuzzy_match(topic, answer):
19
+ """
20
+ Check if a topic sounds like it's in the answer.
21
+ More lenient than strict substring.
22
+ """
23
+ topic_clean = topic.lower().strip()
24
+ answer_clean = answer.lower().strip()
25
+
26
+ # 1. Direct match
27
+ if topic_clean in answer_clean:
28
+ return True
29
+
30
+ # 2. Key word subset check (if all significant words of a topic are in the answer)
31
+ # This helps catch "RAG Pipelines" vs "AI pipelines for RAG"
32
+ stop_words = {"and", "the", "a", "an", "is", "for", "vs", "to", "of", "with"}
33
+ words = [w for w in topic_clean.split() if w not in stop_words]
34
+
35
+ if not words: return False
36
+
37
+ matches = sum(1 for w in words if w in answer_clean)
38
+ # If 75% of the important words are there, count it as a pass
39
+ if (matches / len(words)) >= 0.75:
40
+ return True
41
+
42
+ return False
43
+
44
+ def save_detailed_log(qid, question, answer, chunks, retrieval_pass, missing_topics):
45
+ """Save a clean markdown file for manual human inspection of this specific interaction."""
46
+ os.makedirs(DEBUG_DIR, exist_ok=True)
47
+ filepath = DEBUG_DIR / f"{qid}.md"
48
+
49
+ with open(filepath, "w", encoding="utf-8") as f:
50
+ f.write(f"# Evaluation Log: {qid}\n\n")
51
+ f.write(f"## Question\n{question}\n\n")
52
+ f.write(f"## Status\n")
53
+ f.write(f"- **Retrieval Mode:** {'PASS' if retrieval_pass else 'FAIL'}\n")
54
+ f.write(f"- **Generation Mode:** {'PASS' if not missing_topics else 'FAIL'}\n")
55
+ if missing_topics:
56
+ f.write(f"- **Missing Topics:** {', '.join(missing_topics)}\n")
57
+ f.write(f"\n## ArunCore Answer\n{answer}\n\n")
58
+ f.write(f"## Retrieved Chunks (Final Top 5)\n")
59
+ for i, doc in enumerate(chunks):
60
+ f.write(f"### Chunk {i+1} | Source: {doc.metadata.get('source')}\n")
61
+ f.write(f"```text\n{doc.page_content}\n```\n\n")
62
+
63
+ def run_evaluation():
64
+ print("--- ArunCore Dual-Evaluation Pipeline (Fuzzy Match + Rate Limit Handling) ---")
65
+
66
+ # 1. Initialize Agent
67
+ print("Initializing Agent...")
68
+ try:
69
+ vectorstore, bm25_retriever, compressor, llm, prompt = init_agent()
70
+ except Exception as e:
71
+ print(f"Failed to initialize agent: {e}")
72
+ return
73
+
74
+ # 2. Load Eval Set
75
+ if not EVAL_SET_PATH.exists():
76
+ print(f"Eval set not found at {EVAL_SET_PATH}")
77
+ return
78
+
79
+ with open(EVAL_SET_PATH, "r", encoding="utf-8") as f:
80
+ eval_set = json.load(f)
81
+
82
+ results = []
83
+ passed_retrieval = 0
84
+ passed_generation = 0
85
+ total = len(eval_set)
86
+
87
+ print(f"Starting evaluation of {total} questions...\n")
88
+
89
+ for i, test in enumerate(eval_set):
90
+ qid = test.get("id", f"Q{i}")
91
+ question = test.get("question")
92
+ expected_source = test.get("expected_source")
93
+ expected_topics = test.get("expected_topics", [])
94
+
95
+ print(f"[{i+1}/{total}] Evaluating {qid}: {question[:60]}...")
96
+
97
+ # Execute Agent
98
+ try:
99
+ # We add a delay to satisfy the 10/min Cohere Trial Limit
100
+ if i > 0:
101
+ print(f" (Rate limit cool-down: 6.5s)")
102
+ time.sleep(6.5)
103
+
104
+ response = answer_query(question, vectorstore, bm25_retriever, compressor, llm, prompt)
105
+ answer = response["answer"]
106
+ chunks = response["retrieved_chunks"]
107
+ except Exception as e:
108
+ print(f" Error Querying Agent: {e}")
109
+ results.append({
110
+ "id": qid,
111
+ "status": "ERROR",
112
+ "error": str(e)
113
+ })
114
+ continue
115
+
116
+ # --- Layer 1: Retrieval Check ---
117
+ retrieval_pass = False
118
+ if expected_source.startswith("static/"):
119
+ retrieval_pass = True
120
+ else:
121
+ for doc in chunks:
122
+ source_meta = doc.metadata.get("source", "").lower()
123
+ if expected_source.lower() in source_meta:
124
+ retrieval_pass = True
125
+ break
126
+
127
+ if retrieval_pass: passed_retrieval += 1
128
+
129
+ # --- Layer 2: Generation Check ---
130
+ # Fuzzy match for topics
131
+ missing_topics = []
132
+ for topic in expected_topics:
133
+ if not fuzzy_match(topic, answer):
134
+ missing_topics.append(topic)
135
+
136
+ generation_pass = len(missing_topics) == 0
137
+ if generation_pass: passed_generation += 1
138
+
139
+ # Log detailed human-readable file
140
+ save_detailed_log(qid, question, answer, chunks, retrieval_pass, missing_topics)
141
+
142
+ # Store result in summary list
143
+ results.append({
144
+ "id": qid,
145
+ "retrieval": "PASS" if retrieval_pass else "FAIL",
146
+ "generation": "PASS" if generation_pass else "FAIL",
147
+ "missing": missing_topics
148
+ })
149
+
150
+ # 3. Final Report
151
+ report = {
152
+ "summary": {
153
+ "total_questions": total,
154
+ "retrieval_accuracy": f"{(passed_retrieval/total)*100:.2f}%",
155
+ "generation_accuracy": f"{(passed_generation/total)*100:.2f}%",
156
+ },
157
+ "details": results
158
+ }
159
+
160
+ with open(REPORT_PATH, "w", encoding="utf-8") as f:
161
+ json.dump(report, f, indent=4)
162
+
163
+ print("\n" + "="*40)
164
+ print("EVALUATION COMPLETE")
165
+ print(f"Retrieval Accuracy: {report['summary']['retrieval_accuracy']}")
166
+ print(f"Generation Accuracy: {report['summary']['generation_accuracy']}")
167
+ print(f"Detailed logs saved to: {DEBUG_DIR}")
168
+ print("="*40)
169
+
170
+ if __name__ == "__main__":
171
+ run_evaluation()
backend/app/core/ingest.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+ from dotenv import load_dotenv
8
+ from langchain_openai import OpenAIEmbeddings
9
+ from langchain_community.vectorstores import Chroma
10
+ from langchain_text_splitters import (
11
+ MarkdownHeaderTextSplitter,
12
+ RecursiveCharacterTextSplitter,
13
+ )
14
+ from langchain_core.documents import Document
15
+
16
+ # Load environment variables from .env file if it exists
17
+ load_dotenv()
18
+
19
+ BASE_DIR = Path(__file__).resolve().parent.parent
20
+ DATA_DIR = BASE_DIR / "data"
21
+ DB_DIR = BASE_DIR / "db"
22
+ STATE_FILE = DB_DIR / "ingestion_state.json"
23
+
24
+ # Folders to parse for the Vector DB
25
+ INGEST_DIRS = ["github", "raw", "linkedin", "static"]
26
+
27
+
28
+ def get_file_hash(filepath: Path) -> str:
29
+ with open(filepath, "r", encoding="utf-8") as f:
30
+ return hashlib.md5(f.read().encode("utf-8")).hexdigest()
31
+
32
+
33
+ def load_state() -> Dict[str, Dict]:
34
+ """
35
+ Returns state in this normalized format:
36
+ {
37
+ "data/static/public_profile.md": {
38
+ "hash": "...",
39
+ "chunk_ids": ["static_N/A_public_profile_chunk_0", ...]
40
+ }
41
+ }
42
+
43
+ Supports old state format too:
44
+ {
45
+ "data/static/public_profile.md": "old_md5_hash"
46
+ }
47
+ """
48
+ if not STATE_FILE.exists():
49
+ return {}
50
+
51
+ with open(STATE_FILE, "r", encoding="utf-8") as f:
52
+ raw_state = json.load(f)
53
+
54
+ if isinstance(raw_state, dict) and "files" in raw_state and isinstance(raw_state["files"], dict):
55
+ normalized = {}
56
+ for rel_path, entry in raw_state["files"].items():
57
+ if isinstance(entry, dict):
58
+ normalized[rel_path] = {
59
+ "hash": entry.get("hash", ""),
60
+ "chunk_ids": entry.get("chunk_ids", []),
61
+ }
62
+ return normalized
63
+
64
+ normalized = {}
65
+ if isinstance(raw_state, dict):
66
+ for rel_path, value in raw_state.items():
67
+ if isinstance(value, str):
68
+ normalized[rel_path] = {
69
+ "hash": value,
70
+ "chunk_ids": [],
71
+ }
72
+ elif isinstance(value, dict):
73
+ normalized[rel_path] = {
74
+ "hash": value.get("hash", ""),
75
+ "chunk_ids": value.get("chunk_ids", []),
76
+ }
77
+ return normalized
78
+
79
+
80
+ def save_state(state: Dict[str, Dict]) -> None:
81
+ os.makedirs(DB_DIR, exist_ok=True)
82
+ payload = {
83
+ "version": 2,
84
+ "files": state,
85
+ }
86
+ with open(STATE_FILE, "w", encoding="utf-8") as f:
87
+ json.dump(payload, f, indent=4)
88
+
89
+
90
+ def process_markdown_file(filepath: Path, base_folder: str, rel_path: str) -> List[Document]:
91
+ with open(filepath, "r", encoding="utf-8") as f:
92
+ text = f.read()
93
+
94
+ headers_to_split_on = [
95
+ ("#", "Header 1"),
96
+ ("##", "Header 2"),
97
+ ("###", "Header 3"),
98
+ ]
99
+ markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
100
+ md_header_splits = markdown_splitter.split_text(text)
101
+
102
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=150)
103
+ splits = text_splitter.split_documents(md_header_splits)
104
+
105
+ safe_name = filepath.stem.replace(" ", "_").replace(".", "_")
106
+
107
+ project_name = "N/A"
108
+ parts = list(filepath.relative_to(DATA_DIR).parts)
109
+ if "github" in parts and len(parts) > 1:
110
+ project_name = parts[1]
111
+
112
+ for i, split in enumerate(splits):
113
+ split.metadata["source"] = rel_path
114
+ split.metadata["folder"] = base_folder
115
+ split.metadata["project"] = project_name
116
+ split.metadata["chunk_id"] = f"{base_folder}_{project_name}_{safe_name}_chunk_{i}"
117
+
118
+ return splits
119
+
120
+
121
+ def process_json_file(filepath: Path, base_folder: str, rel_path: str) -> List[Document]:
122
+ with open(filepath, "r", encoding="utf-8") as f:
123
+ try:
124
+ data = json.load(f)
125
+ except Exception:
126
+ data = None
127
+
128
+ if filepath.name == "unknown_questions.json" and isinstance(data, list):
129
+ docs = []
130
+ for i, item in enumerate(data):
131
+ if isinstance(item, dict) and "question" in item and "answer" in item:
132
+ q = item.get("question", "").strip()
133
+ a = item.get("answer", "").strip()
134
+ content = f"### Verified Question & Answer\n**Question:** {q}\n**Answer:** {a}"
135
+ docs.append(
136
+ Document(
137
+ page_content=content,
138
+ metadata={
139
+ "source": rel_path,
140
+ "folder": base_folder,
141
+ "project": "unknown_questions",
142
+ "chunk_id": f"unknown_q_{i}",
143
+ },
144
+ )
145
+ )
146
+ if docs:
147
+ return docs
148
+
149
+ with open(filepath, "r", encoding="utf-8") as f:
150
+ content = f.read()
151
+
152
+ project_name = "N/A"
153
+ parts = list(filepath.relative_to(DATA_DIR).parts)
154
+ if "github" in parts and len(parts) > 1:
155
+ project_name = parts[1]
156
+
157
+ safe_name = filepath.stem.replace(" ", "_").replace(".", "_")
158
+ chunk_id = f"{base_folder}_{project_name}_{safe_name}_chunk_0"
159
+
160
+ doc = Document(
161
+ page_content=content,
162
+ metadata={
163
+ "source": rel_path,
164
+ "folder": base_folder,
165
+ "project": project_name,
166
+ "chunk_id": chunk_id,
167
+ },
168
+ )
169
+ return [doc]
170
+
171
+
172
+ def process_file(filepath: Path, base_folder: str, rel_path: str) -> List[Document]:
173
+ if filepath.suffix == ".md":
174
+ return process_markdown_file(filepath, base_folder, rel_path)
175
+ if filepath.suffix == ".json":
176
+ return process_json_file(filepath, base_folder, rel_path)
177
+ return []
178
+
179
+
180
+ def main():
181
+ if not os.getenv("OPENAI_API_KEY"):
182
+ print("\n[ERROR] OPENAI_API_KEY not found in the environment.")
183
+ print("Please set it or create a .env file containing OPENAI_API_KEY=your_key_here")
184
+ return
185
+
186
+ print("Initializing ChromaDB and OpenAI Embeddings...")
187
+ embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
188
+
189
+ vectorstore = Chroma(
190
+ collection_name="aruncore_knowledge",
191
+ embedding_function=embeddings,
192
+ persist_directory=str(DB_DIR),
193
+ )
194
+
195
+ previous_state = load_state()
196
+ new_state: Dict[str, Dict] = {}
197
+
198
+ docs_to_upsert: List[Document] = []
199
+ ids_to_upsert: List[str] = []
200
+ stale_ids_to_delete = set()
201
+ current_rel_paths = set()
202
+
203
+ folders_to_scan = [folder for folder in INGEST_DIRS if (DATA_DIR / folder).is_dir()]
204
+
205
+ for folder in folders_to_scan:
206
+ target_dir = DATA_DIR / folder
207
+
208
+ for ext in ["*.md", "*.json"]:
209
+ for filepath in target_dir.rglob(ext):
210
+ if "test_set" in filepath.parts:
211
+ continue
212
+
213
+ rel_path = filepath.relative_to(BASE_DIR).as_posix()
214
+ current_rel_paths.add(rel_path)
215
+
216
+ file_hash = get_file_hash(filepath)
217
+ previous_entry = previous_state.get(rel_path, {})
218
+ previous_hash = previous_entry.get("hash")
219
+ previous_chunk_ids = previous_entry.get("chunk_ids", [])
220
+
221
+ is_unchanged = (
222
+ previous_hash == file_hash
223
+ and isinstance(previous_chunk_ids, list)
224
+ and len(previous_chunk_ids) > 0
225
+ )
226
+
227
+ if is_unchanged:
228
+ new_state[rel_path] = {
229
+ "hash": previous_hash,
230
+ "chunk_ids": previous_chunk_ids,
231
+ }
232
+ continue
233
+
234
+ print(f"File changed/new -> Processing: {rel_path}")
235
+
236
+ splits = process_file(filepath, folder, rel_path)
237
+ new_chunk_ids = [doc.metadata["chunk_id"] for doc in splits]
238
+
239
+ docs_to_upsert.extend(splits)
240
+ ids_to_upsert.extend(new_chunk_ids)
241
+
242
+ old_ids_set = set(previous_chunk_ids)
243
+ new_ids_set = set(new_chunk_ids)
244
+
245
+ stale_ids_to_delete.update(old_ids_set - new_ids_set)
246
+
247
+ new_state[rel_path] = {
248
+ "hash": file_hash,
249
+ "chunk_ids": new_chunk_ids,
250
+ }
251
+
252
+ deleted_files = set(previous_state.keys()) - current_rel_paths
253
+ for rel_path in deleted_files:
254
+ old_chunk_ids = previous_state.get(rel_path, {}).get("chunk_ids", [])
255
+ if old_chunk_ids:
256
+ print(f"File deleted -> Removing old chunks: {rel_path}")
257
+ stale_ids_to_delete.update(old_chunk_ids)
258
+
259
+ if docs_to_upsert:
260
+ print(f"\nUpserting {len(docs_to_upsert)} new/modified chunks into Vector DB...")
261
+ vectorstore.add_documents(documents=docs_to_upsert, ids=ids_to_upsert)
262
+ print("Upsert complete.")
263
+ else:
264
+ print("\nNo new or modified files to upsert.")
265
+
266
+ if stale_ids_to_delete:
267
+ stale_ids_list = sorted(stale_ids_to_delete)
268
+ print(f"Deleting {len(stale_ids_list)} stale chunks from Vector DB...")
269
+ vectorstore.delete(ids=stale_ids_list)
270
+ print("Stale chunk cleanup complete.")
271
+ else:
272
+ print("No stale chunks to delete.")
273
+
274
+ save_state(new_state)
275
+ print("Ingestion sequence complete.")
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()
backend/app/main.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from backend.app.api.v1.router import api_v1_router
5
+ from backend.app.core.api import app as legacy_app
6
+
7
+ app = legacy_app
8
+
9
+ # Mount versioned API routes
10
+ app.include_router(api_v1_router)
11
+
12
+ if __name__ == "__main__":
13
+ import uvicorn
14
+ port = int(os.getenv("PORT", "8000"))
15
+ uvicorn.run("backend.app.main:app", host="0.0.0.0", port=port, reload=True)
backend/app/schemas/chat.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class ChatRequest(BaseModel):
6
+ message: str = Field(..., min_length=1, description="User input message text")
7
+ session_id: str = Field(..., description="Unique session identifier")
8
+ tutor_id: Optional[str] = Field("arun", description="Optional tenant or tutor ID")
9
+
10
+
11
+ class ChatMessageEntry(BaseModel):
12
+ id: str
13
+ sender: str
14
+ name: str
15
+ text: str
16
+ timestamp: str
17
+ thoughts: Optional[List[str]] = None
18
+
19
+
20
+ class ChatHistoryResponse(BaseModel):
21
+ session_id: str
22
+ messages: List[ChatMessageEntry] = Field(default_factory=list)
23
+
24
+
25
+ class NDJSONStreamChunk(BaseModel):
26
+ type: str = Field(..., description="'status', 'token', 'final', or 'error'")
27
+ content: Optional[str] = None
28
+ reply: Optional[str] = None
29
+ thoughts: Optional[List[str]] = None
30
+ session_id: Optional[str] = None
backend/app/schemas/tenant.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class BrandConfig(BaseModel):
6
+ tutor_id: str = Field(..., description="Tenant ID matching directory name")
7
+ name: str = Field("Arun Yadav", description="Tenant full name")
8
+ title: str = Field("Arun's AI Assistant", description="Hero assistant title")
9
+ role: str = Field("AI Systems Architect β€’ Healthcare & Education", description="Hero role subtitle")
10
+ subtitle: Optional[str] = None
11
+ avatar_url: str = Field("/profile_photo.png", description="Avatar image URL")
12
+ logo_url: str = Field("/logo.jpg", description="Brand logo URL")
13
+ primary_color: str = Field("#6366f1", description="Primary brand hex color")
14
+ accent_color: str = Field("#818cf8", description="Accent brand hex color")
15
+ theme: str = Field("dark", description="Light or dark theme preference")
16
+ cta_text: str = Field("Consult Arun", description="CTA button label")
17
+
18
+
19
+ class AgentConfig(BaseModel):
20
+ tutor_id: str
21
+ system_prompt: str = Field(..., description="Base system prompt")
22
+ temperature: float = Field(0.3, ge=0.0, le=1.0)
23
+ model: str = Field("gpt-4o", description="LLM model identifier")
24
+ enabled_tools: List[str] = Field(default_factory=list, description="Array of enabled tool names")
25
+ guardrails: List[str] = Field(default_factory=list, description="Safety guardrails")
26
+
27
+
28
+ class ChatConfig(BaseModel):
29
+ tutor_id: str
30
+ welcome_message: str = Field(..., description="Hero card welcome text")
31
+ suggested_questions: List[str] = Field(default_factory=list, description="Chips suggested questions")
32
+
33
+
34
+ class VoiceConfig(BaseModel):
35
+ tutor_id: str
36
+ voice_id: str = Field("alloy", description="TTS voice identifier")
37
+ model: str = Field("tts-1", description="TTS model")
38
+ speed: float = Field(1.0, ge=0.25, le=4.0)
39
+
40
+
41
+ class SEOConfig(BaseModel):
42
+ tutor_id: str
43
+ meta_title: str
44
+ meta_description: str
45
+ keywords: List[str] = Field(default_factory=list)
46
+
47
+
48
+ class SocialConfig(BaseModel):
49
+ tutor_id: str
50
+ website: Optional[str] = None
51
+ linkedin: Optional[str] = None
52
+ udemy: Optional[str] = None
53
+ github: Optional[str] = None
54
+ twitter: Optional[str] = None
55
+
56
+
57
+ class TenantFullConfig(BaseModel):
58
+ tutor_id: str
59
+ brand: BrandConfig
60
+ agent: AgentConfig
61
+ chat: ChatConfig
62
+ voice: VoiceConfig
63
+ seo: SEOConfig
64
+ social: SocialConfig
65
+
66
+ def to_legacy_dict(self) -> Dict[str, Any]:
67
+ """Maps split JSON configs back to existing frontend expected dictionary structure (100% backward compatible!)."""
68
+ return {
69
+ "tutor_id": self.tutor_id,
70
+ "title": self.brand.title,
71
+ "subtitle": self.brand.subtitle or self.brand.role,
72
+ "role": self.brand.role,
73
+ "name": self.brand.name,
74
+ "avatar": self.brand.avatar_url,
75
+ "logo": self.brand.logo_url,
76
+ "welcome_message": self.chat.welcome_message,
77
+ "suggested_questions": self.chat.suggested_questions,
78
+ "cta_text": self.brand.cta_text,
79
+ "primary_color": self.brand.primary_color,
80
+ "accent_color": self.brand.accent_color,
81
+ "system_prompt": self.agent.system_prompt,
82
+ "enabled_tools": self.agent.enabled_tools,
83
+ "frontend_ui_dictionary": {
84
+ "header": {
85
+ "profile_name": self.brand.name,
86
+ "profile_badge": self.brand.role,
87
+ "cta_button": {
88
+ "text": self.brand.cta_text,
89
+ },
90
+ },
91
+ "sidebar": {
92
+ "profile_card": {
93
+ "name": self.brand.name,
94
+ "title": self.brand.role,
95
+ "sub_badge": f"{self.brand.name}'s AI Twin",
96
+ },
97
+ "contact_button": {
98
+ "title": self.brand.cta_text,
99
+ "subtitle": f"Connect directly with {self.brand.name}",
100
+ },
101
+ },
102
+ "chat_panel": {
103
+ "hero_card": {
104
+ "assistant_title": self.brand.title,
105
+ "role_subtitle": self.brand.role,
106
+ "welcome_paragraph": self.chat.welcome_message,
107
+ "cta_button_text": self.brand.cta_text,
108
+ },
109
+ "input_bar": {
110
+ "placeholder": f"Ask {self.brand.name}'s AI Assistant...",
111
+ },
112
+ "suggested_questions_section": {
113
+ "chips": [{"query": q} for q in self.chat.suggested_questions],
114
+ },
115
+ }
116
+ },
117
+ "client_metadata": {
118
+ "tutor_id": self.tutor_id,
119
+ "avatar_url": self.brand.avatar_url,
120
+ "logo_url": self.brand.logo_url,
121
+ }
122
+ }
backend/app/schemas/voice.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class TTSRequest(BaseModel):
6
+ text: str = Field(..., min_length=1, max_length=2000, description="Text snippet to synthesize into audio")
7
+ voice: Optional[str] = Field("alloy", description="TTS voice identifier")
8
+ tutor_id: Optional[str] = Field("arun", description="Optional tenant identifier")
backend/app/schemas/webhook.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class TelegramUpdate(BaseModel):
6
+ update_id: int
7
+ message: Optional[Dict[str, Any]] = None
8
+ callback_query: Optional[Dict[str, Any]] = None
9
+
10
+
11
+ class ActiveLearningWebhookPayload(BaseModel):
12
+ session_id: str
13
+ user_question: str
14
+ owner_answer: str
15
+ tutor_id: Optional[str] = "arun"
backend/app/services/active_learning_service.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from typing import Optional, Dict, Any
4
+ from backend.app.services.rag_service import RAGService
5
+
6
+
7
+ class ActiveLearningService:
8
+ def __init__(self, tenant_id: str = "arun"):
9
+ self.tenant_id = tenant_id
10
+ self.rag_service = RAGService(tenant_id=tenant_id)
11
+
12
+ def process_incoming_owner_reply(self, session_id: str, question: str, answer: str) -> bool:
13
+ """Processes owner's Telegram/WhatsApp answer, ingests into active_learning.json & ChromaDB."""
14
+ print(f"[ACTIVE_LEARNING] Processing owner answer for session '{session_id}': Q: '{question}' βž” A: '{answer}'")
15
+
16
+ # Ingest into vector store
17
+ success = self.rag_service.add_knowledge_entry(question, answer)
18
+ return success
backend/app/services/agent_runner.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent execution loop (the ReAct harness).
2
+
3
+ Runs the recursive tool-call loop for the ArunCore digital twin: request ->
4
+ optional escalation -> up to N tool iterations -> token-by-token streaming of
5
+ the final synthesis. Also owns the 3-way live human takeover answer trigger.
6
+ This is where the previous `/chat` monolith body now lives, fully reusable by
7
+ the FastAPI layer, the Telegram bot vector, and future channels.
8
+ """
9
+ import asyncio
10
+ import json
11
+ from typing import Any, AsyncGenerator, Dict, List, Optional
12
+
13
+ from langchain_core.messages import SystemMessage
14
+
15
+ from backend.app.services.session_store import SessionStore, session_store
16
+ from backend.app.services.prompt_builder import PromptBuilder
17
+
18
+
19
+ def run_pre_escalation(user_input: str, tool_map: dict, user_metadata: Optional[dict] = None, fast: bool = False) -> Dict[str, Any]:
20
+ """Checks for direct-contact intent that should immediately ping Arun."""
21
+ lowered = (user_input or "").lower()
22
+ urgent_keywords = [
23
+ "hire", "contact", "talk to arun", "call arun", "meet arun",
24
+ "whatsapp", "urgent", "consult", "project inquiry", "work together",
25
+ ]
26
+
27
+ for kw in urgent_keywords:
28
+ if kw in lowered:
29
+ return {"escalate": True, "reason": f"Urgent contact keyword detected: '{kw}'"}
30
+
31
+ return {"escalate": False, "reason": ""}
32
+
33
+
34
+ class AgentRunner:
35
+ """Orchestrates agentic streaming responses for a single chat request."""
36
+
37
+ def __init__(
38
+ self,
39
+ store: Optional[SessionStore] = None,
40
+ agent_factory: Any = None,
41
+ max_iterations: int = 7,
42
+ max_search_limit: int = 7,
43
+ ):
44
+ self.store = store or session_store
45
+ self.agent_factory = agent_factory
46
+ self.max_iterations = max_iterations
47
+ self.max_search_limit = max_search_limit
48
+
49
+ async def stream_chat(
50
+ self,
51
+ session_id: str,
52
+ message: str,
53
+ tutor_id: Optional[str] = None,
54
+ tool_map: Optional[Dict[str, Any]] = None,
55
+ ) -> AsyncGenerator[Dict[str, Any], None]:
56
+ """Yields NDJSON-able chunks: {'type': status|token|final|error}."""
57
+ thoughts: List[str] = []
58
+ try:
59
+ from backend.app.services.notification_service import (
60
+ queue_debug_event,
61
+ queue_maybe_notify_arun,
62
+ queue_chat_history_to_telegram,
63
+ queue_automated_chat_alert,
64
+ )
65
+
66
+ if self.agent_factory is None:
67
+ from backend.app.core.agent import init_agent
68
+
69
+ self.agent_factory = init_agent
70
+
71
+ self.store.record_message(session_id, "user", message)
72
+
73
+ queue_debug_event(
74
+ "user_message",
75
+ message,
76
+ {"channel": "api", "session_id": session_id, "tutor_id": tutor_id},
77
+ )
78
+
79
+ # 3-way live chat takeover: real Arun is in control, AI Twin pauses.
80
+ if self.store.is_human_control(session_id):
81
+ queue_automated_chat_alert(
82
+ session_id=session_id,
83
+ user_input=message,
84
+ assistant_response="[Real Arun is currently in live control of this chat session. AI Twin paused.]",
85
+ )
86
+ yield {"type": "status", "content": "🟒 Real Arun is in control of this session. AI Twin paused. Waiting for Real Arun or /answer command..."}
87
+ yield {
88
+ "type": "final",
89
+ "reply": "",
90
+ "thoughts": ["Real Arun in live control. AI Twin paused."],
91
+ "session_id": session_id,
92
+ }
93
+ return
94
+
95
+ yield {"type": "status", "content": "Analyzing request & retrieving context..."}
96
+ thoughts.append("Analyzing request & retrieving context...")
97
+
98
+ pre_result = run_pre_escalation(message, tool_map or {})
99
+ if pre_result.get("escalate"):
100
+ yield {"type": "status", "content": "Triggering instant Telegram alert..."}
101
+ thoughts.append("Triggering instant Telegram alert...")
102
+ queue_maybe_notify_arun(
103
+ user_input=message,
104
+ reason=pre_result.get("reason"),
105
+ channel="api",
106
+ session_id=session_id,
107
+ )
108
+
109
+ session_llm, session_prompt, _, _ = self.agent_factory(tutor_id=tutor_id)
110
+ memory = self.store.get_or_create_memory(session_id)
111
+
112
+ scratchpad: List[Any] = []
113
+ iterations = 0
114
+ search_count = 0
115
+ final_response = ""
116
+ executed_tools: List[str] = []
117
+ retrieved_chunks: List[str] = []
118
+ github_data: List[str] = []
119
+
120
+ while iterations < self.max_iterations:
121
+ messages = session_prompt.format_messages(
122
+ running_summary=memory.running_summary,
123
+ chat_history=memory.get_messages(),
124
+ input=message,
125
+ agent_scratchpad=scratchpad,
126
+ )
127
+
128
+ # Inject dynamic Real Human Presence notice if the real human is active.
129
+ arun_human_msgs = self.store.get_human_entries(session_id)
130
+ if arun_human_msgs:
131
+ messages = PromptBuilder.inject_live_human_notice(messages, arun_human_msgs)
132
+
133
+ ai_msg = await asyncio.to_thread(session_llm.invoke, messages)
134
+
135
+ if ai_msg.tool_calls:
136
+ scratchpad.append(ai_msg)
137
+ for tc in ai_msg.tool_calls:
138
+ tool_name = tc["name"]
139
+ tool_args = tc.get("args", {})
140
+
141
+ status_msg = (
142
+ "Searching Arun's knowledge base..." if tool_name == "search_arun_knowledge"
143
+ else "Sending notification to Arun..." if tool_name == "notify_arun"
144
+ else f"Executing {tool_name}..."
145
+ )
146
+
147
+ yield {"type": "status", "content": status_msg}
148
+ thoughts.append(status_msg)
149
+ executed_tools.append(f"{tool_name}({tool_args})")
150
+
151
+ if tool_name == "search_arun_knowledge":
152
+ search_count += 1
153
+
154
+ if search_count > self.max_search_limit:
155
+ tool_result = f"Search limit reached ({self.max_search_limit}). Finalizing based on existing context."
156
+ else:
157
+ tool_func = (tool_map or {}).get(tool_name)
158
+ tool_result = await asyncio.to_thread(tool_func.invoke, tool_args) if tool_func else f"Unknown tool: {tool_name}"
159
+
160
+ if tool_name == "search_arun_knowledge" and tool_result:
161
+ retrieved_chunks.append(str(tool_result)[:1000])
162
+ elif tool_name == "get_github_live_data" and tool_result:
163
+ github_data.append(str(tool_result)[:1000])
164
+
165
+ scratchpad.append({
166
+ "role": "tool",
167
+ "name": tool_name,
168
+ "tool_call_id": tc["id"],
169
+ "content": str(tool_result)[:15000],
170
+ })
171
+ iterations += 1
172
+ else:
173
+ yield {"type": "status", "content": "Synthesizing final response..."}
174
+ thoughts.append("Synthesizing final response...")
175
+
176
+ full_reply = ""
177
+ async for chunk in self._stream_tokens(session_llm, messages):
178
+ yield chunk
179
+ if isinstance(chunk, dict) and chunk.get("type") == "token":
180
+ full_reply += chunk["content"]
181
+
182
+ final_response = full_reply
183
+ break
184
+
185
+ if not final_response:
186
+ yield {"type": "status", "content": "Synthesizing final response..."}
187
+ thoughts.append("Synthesizing final response...")
188
+ messages_fallback = session_prompt.format_messages(
189
+ running_summary=memory.running_summary,
190
+ chat_history=memory.get_messages(),
191
+ input=message,
192
+ agent_scratchpad=scratchpad,
193
+ )
194
+ full_reply = ""
195
+ async for chunk in self._stream_tokens(session_llm, messages_fallback):
196
+ yield chunk
197
+ if isinstance(chunk, dict) and chunk.get("type") == "token":
198
+ full_reply += chunk["content"]
199
+ final_response = full_reply
200
+
201
+ memory.add_interaction(message, final_response)
202
+ self.store.record_message(session_id, "twin", final_response, thoughts=thoughts)
203
+
204
+ queue_chat_history_to_telegram(
205
+ session_id=session_id,
206
+ user_input=message,
207
+ assistant_response=final_response,
208
+ thoughts=thoughts,
209
+ tool_calls=executed_tools,
210
+ retrieved_chunks=retrieved_chunks,
211
+ github_data=github_data,
212
+ )
213
+
214
+ # Unconditionally queue 100% automated chat alert for EVERY chat.
215
+ queue_automated_chat_alert(
216
+ session_id=session_id,
217
+ user_input=message,
218
+ assistant_response=final_response,
219
+ )
220
+
221
+ yield {
222
+ "type": "final",
223
+ "reply": final_response,
224
+ "thoughts": thoughts,
225
+ "session_id": session_id,
226
+ }
227
+
228
+ except Exception as err:
229
+ err_msg = f"API Error: {str(err)}"
230
+ yield {"type": "error", "content": err_msg}
231
+
232
+ async def trigger_ai_answer(self, session_id: str, extra_prompt: str = "") -> str:
233
+ """Synthesize an AI answer after the /answer command from the real human."""
234
+ from backend.app.core.agent import init_agent
235
+
236
+ memory = self.store.get_or_create_memory(session_id)
237
+ main_llm, prompt, _, _ = init_agent()
238
+
239
+ session_msgs = self.store.get_history(session_id)
240
+ transcript_lines = []
241
+ last_user_input = ""
242
+ for m in session_msgs:
243
+ sender_label = (
244
+ "Visitor" if m.get("sender") == "user"
245
+ else "Real Arun Yadav (πŸ‘¨β€πŸ’»)" if m.get("sender") == "human_arun"
246
+ else "Arun's AI Assistant"
247
+ )
248
+ transcript_lines.append(f"{sender_label}: {m.get('text')}")
249
+ if m.get("sender") == "user":
250
+ last_user_input = m.get("text")
251
+
252
+ full_transcript = "\n".join(transcript_lines)
253
+
254
+ system_notice = SystemMessage(content=(
255
+ f"🟒 3-WAY CHAT INSTRUCTION FOR AI TWIN:\n"
256
+ f"Real Arun Yadav issued the /answer command for you to respond to the visitor's question.\n"
257
+ f"Read the full 3-party conversation transcript below (Visitor, Real Arun Yadav, and AI Twin):\n\n"
258
+ f"--- FULL 3-WAY CHAT TRANSCRIPT ---\n{full_transcript}\n\n"
259
+ f"MANDATORY INSTRUCTIONS FOR AI TWIN:\n"
260
+ f"1. Synthesize all context from the visitor's question and Real Arun Yadav's live comments.\n"
261
+ f"2. Generate an accurate, helpful, and natural response for the visitor.\n"
262
+ f"3. Acknowledge Real Arun Yadav's live presence if relevant."
263
+ ))
264
+
265
+ messages = prompt.format_messages(
266
+ running_summary=memory.running_summary,
267
+ chat_history=memory.get_messages(),
268
+ input=extra_prompt or last_user_input or "Please answer the visitor's question based on our 3-way conversation.",
269
+ agent_scratchpad=[],
270
+ )
271
+ messages.insert(1, system_notice)
272
+
273
+ ai_msg = await asyncio.to_thread(main_llm.invoke, messages)
274
+ final_reply = ai_msg.content.strip()
275
+
276
+ if final_reply:
277
+ self.store.record_message(
278
+ session_id, "twin", final_reply,
279
+ thoughts=["AI Twin triggered via /answer command."],
280
+ )
281
+ memory.add_interaction(last_user_input or "Visitor Query", final_reply)
282
+
283
+ return final_reply
284
+
285
+ async def _stream_tokens(self, llm, messages):
286
+ """Yields {'type':'token', 'content': ...} chunks by streaming the LLM.
287
+
288
+ Mirrors the original implementation: the sync `llm.stream` generator is
289
+ consumed in place so tokens arrive incrementally over the wire.
290
+ """
291
+ for chunk in llm.stream(messages):
292
+ if getattr(chunk, "content", None):
293
+ yield {"type": "token", "content": chunk.content}
294
+ await asyncio.sleep(0.005)
295
+
296
+ def sync_reply(
297
+ self,
298
+ session_id: str,
299
+ user_input: str,
300
+ llm: Any,
301
+ prompt: Any,
302
+ memory: Any,
303
+ tool_map: Optional[Dict[str, Any]] = None,
304
+ user_metadata: Optional[Dict[str, Any]] = None,
305
+ max_iterations: int = 3,
306
+ ) -> str:
307
+ """Blocking agent turn for non-streaming channels (Telegram bot).
308
+
309
+ Replicates the legacy bot loop exactly (3 iterations, tool errors
310
+ swallowed into the reply context, final fallback message).
311
+ """
312
+ from backend.app.services.notification_service import (
313
+ queue_debug_event,
314
+ queue_maybe_notify_arun,
315
+ )
316
+
317
+ scratchpad: List[Any] = []
318
+ tool_map = tool_map or {}
319
+
320
+ try:
321
+ queue_debug_event(
322
+ "user_message",
323
+ user_input,
324
+ {"channel": "telegram", "chat_id": session_id, **(user_metadata or {})},
325
+ )
326
+
327
+ pre_escalation = run_pre_escalation(
328
+ user_input,
329
+ tool_map,
330
+ {"channel": "telegram", "chat_id": session_id, **(user_metadata or {})},
331
+ False,
332
+ )
333
+ if pre_escalation:
334
+ queue_debug_event(
335
+ "pre_escalation",
336
+ pre_escalation.get("result", ""),
337
+ {
338
+ "channel": "telegram",
339
+ "chat_id": session_id,
340
+ "category": pre_escalation.get("category"),
341
+ "reason": pre_escalation.get("reason"),
342
+ **(user_metadata or {}),
343
+ },
344
+ )
345
+
346
+ final_response = None
347
+
348
+ for _ in range(max_iterations):
349
+ messages = prompt.format_messages(
350
+ running_summary=memory.running_summary,
351
+ chat_history=memory.get_messages(),
352
+ input=user_input,
353
+ agent_scratchpad=scratchpad,
354
+ )
355
+ ai_msg = llm.invoke(messages)
356
+
357
+ if ai_msg.tool_calls:
358
+ scratchpad.append(ai_msg)
359
+ for tc in ai_msg.tool_calls:
360
+ tool_name = tc["name"]
361
+ tool_args = tc.get("args", {})
362
+ queue_debug_event(
363
+ "tool_call",
364
+ json.dumps(tool_args, ensure_ascii=False, indent=2, default=str),
365
+ {
366
+ "channel": "telegram",
367
+ "chat_id": session_id,
368
+ "tool_name": tool_name,
369
+ **(user_metadata or {}),
370
+ },
371
+ )
372
+
373
+ tool_func = tool_map.get(tool_name)
374
+ try:
375
+ result = tool_func.invoke(tool_args)
376
+ except Exception as e:
377
+ result = f"Tool error: {e}"
378
+
379
+ scratchpad.append({
380
+ "role": "tool",
381
+ "name": tool_name,
382
+ "tool_call_id": tc["id"],
383
+ "content": str(result)[:2000],
384
+ })
385
+ queue_debug_event(
386
+ "tool_result",
387
+ str(result),
388
+ {
389
+ "channel": "telegram",
390
+ "chat_id": session_id,
391
+ "tool_name": tool_name,
392
+ **(user_metadata or {}),
393
+ },
394
+ )
395
+ else:
396
+ final_response = ai_msg.content
397
+ break
398
+
399
+ if not final_response:
400
+ final_response = "I ran into an issue internally. Please try again."
401
+
402
+ queue_debug_event(
403
+ "assistant_reply",
404
+ final_response,
405
+ {"channel": "telegram", "chat_id": session_id, **(user_metadata or {})},
406
+ )
407
+
408
+ queue_maybe_notify_arun(
409
+ user_input=user_input,
410
+ final_response=final_response,
411
+ scratchpad=scratchpad,
412
+ tool_map=tool_map,
413
+ user_metadata={"channel": "telegram", "chat_id": session_id, **(user_metadata or {})},
414
+ pre_notified=bool(pre_escalation and pre_escalation.get("handled")),
415
+ )
416
+
417
+ memory.add_interaction(user_input, final_response)
418
+ return final_response
419
+ except Exception as e:
420
+ queue_debug_event(
421
+ "error",
422
+ str(e),
423
+ {"channel": "telegram", "chat_id": session_id, **(user_metadata or {})},
424
+ )
425
+ raise
426
+
427
+
428
+ agent_runner = AgentRunner(agent_factory=None)
backend/app/services/auth_service.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Admin authentication for the 3-way live human chat takeover.
2
+
3
+ Real Arun logs in through a signed admin token embedded in the 1-Click Join
4
+ link generated by Telegram alerts. The token is derived from the session id
5
+ plus a shared secret, keeping the webhook/join flow stateless.
6
+ """
7
+ import hashlib
8
+ import os
9
+
10
+ ADMIN_SECRET_KEY = os.getenv("ADMIN_SECRET_KEY", "aruncore_secret_key_2026")
11
+
12
+
13
+ def generate_admin_token(session_id: str) -> str:
14
+ raw = f"{session_id}:{ADMIN_SECRET_KEY}".encode("utf-8")
15
+ return hashlib.sha256(raw).hexdigest()[:16]
16
+
17
+
18
+ def verify_admin_token(session_id: str, token: str) -> bool:
19
+ if not session_id or not token:
20
+ return False
21
+ expected = generate_admin_token(session_id)
22
+ return token.strip() == expected
backend/app/services/background.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Single-responsibility background task queue.
2
+
3
+ The ArunCore backend performs several fire-and-forget jobs (Telegram delivery,
4
+ chat history logging, debug event logging, re-ingestion triggering). All of
5
+ that work is submitted here instead of being swallowed by the request /
6
+ agent loop, so slow network calls never block chat streaming.
7
+ """
8
+ import queue
9
+ import threading
10
+ from typing import Any, Callable
11
+
12
+
13
+ _task_queue: "queue.Queue[Any]" = queue.Queue()
14
+
15
+
16
+ def _background_worker() -> None:
17
+ import urllib3
18
+
19
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
20
+
21
+ while True:
22
+ try:
23
+ task = _task_queue.get()
24
+ if task is None:
25
+ break
26
+ func, args, kwargs = task
27
+ try:
28
+ func(*args, **kwargs)
29
+ except Exception as e:
30
+ print(f"[BACKGROUND WORKER ERROR] Failed in {getattr(func, '__name__', func)}: {e}")
31
+ finally:
32
+ _task_queue.task_done()
33
+ except Exception as outer_e:
34
+ print(f"[BACKGROUND WORKER FATAL] Queue fetch failed: {outer_e}")
35
+
36
+
37
+ _thread = threading.Thread(target=_background_worker, daemon=True, name="aruncore-background")
38
+ _thread.start()
39
+
40
+
41
+ def submit_background_task(name: str, func: Callable, *args: Any, **kwargs: Any) -> bool:
42
+ """Enqueue a callable to run on the shared background worker thread."""
43
+ try:
44
+ _task_queue.put((func, args, kwargs))
45
+ print(f"[BACKGROUND] {name}: Task queued.")
46
+ return True
47
+ except Exception as e:
48
+ print(f"[BACKGROUND ERROR] Failed to queue {name}: {e}")
49
+ return False
backend/app/services/knowledge_service.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Knowledge retrieval layer of the ArunCore digital twin.
2
+
3
+ Owns every read against the on-disk knowledge base:
4
+
5
+ * `data/github/<repo>/README.md` historical READMEs
6
+ * `data/linkedin/posts.md` scraped posts
7
+ * `data/static/*.md` profile, rules of engagement
8
+ * `data/raw/personal_background.md`
9
+ * `data/raw/unknown_questions.json` verified Q&A pairs
10
+
11
+ It also handles live GitHub lookups and persists newly learned Q&A pairs
12
+ (the active-learning feedback loop).
13
+ """
14
+ import os
15
+ import re
16
+ import json
17
+ import datetime
18
+ import subprocess
19
+ from typing import Dict, Any, List, Optional, Tuple
20
+
21
+ import requests
22
+
23
+ from backend.app.services.notification_service import safe_truncate, schedule_notify_arun
24
+
25
+
26
+ def _project_root() -> str:
27
+ """profile/ (the repository root holding `data/` and `backend/`)."""
28
+ return os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
29
+
30
+
31
+ # ------------------------------------------------------------------ #
32
+ # Chunking + relevance scoring helpers #
33
+ # ------------------------------------------------------------------ #
34
+ _QUERY_STOP_WORDS = {
35
+ "how", "does", "the", "a", "an", "is", "for", "to", "of", "with", "work",
36
+ "what", "tell", "me", "about", "can", "you", "who", "where", "why", "arun",
37
+ "and", "or", "that", "this", "it", "in", "on", "at", "your", "my", "i", "we",
38
+ }
39
+
40
+ _BOILERPLATE_LINE_RE = re.compile(
41
+ r"^(>\s*\*\*GitHub Repository:|\*\*Primary Language:|>\s*\*\*Primary Language:|"
42
+ r">\s*\*\*Description:|>\s*\*\*Stars:|\*\*GitHub|#\s+[\w_-]+\s*$)"
43
+ )
44
+
45
+
46
+ def _query_terms(cleaned_query: str) -> List[str]:
47
+ """Significant query words (stopwords removed, len>2)."""
48
+
49
+ def _tok(w: str) -> str:
50
+ return w.strip("?.,!;:'\"()[]{}")
51
+
52
+ return [
53
+ _tok(w) for w in cleaned_query.split()
54
+ if _tok(w) and _tok(w).lower() not in _QUERY_STOP_WORDS and len(_tok(w)) > 2
55
+ ]
56
+
57
+
58
+ def _relevance(score: int, alpha: float = 1.0) -> float:
59
+ """Normalize a raw hit count into a 0..˜2 ordering score."""
60
+ return alpha * (1.0 + min(score, 6))
61
+
62
+
63
+ def _term_hits(text: str, terms: List[str]) -> int:
64
+ """Count word-boundary, case-insensitive term occurrences."""
65
+ if not terms or not text:
66
+ return 0
67
+ lowered = text.lower()
68
+ hits = 0
69
+ for term in terms:
70
+ hits += len(re.findall(rf"(?<![a-z0-9]){re.escape(term)}(?![a-z0-9])", lowered))
71
+ return hits
72
+
73
+
74
+ def _term_score(heading: str, body: str, terms: List[str]) -> int:
75
+ """Weighted relevance: heading hits count double body hits."""
76
+ if not terms:
77
+ return 0
78
+ return 2 * _term_hits(heading, terms) + _term_hits(body, terms)
79
+
80
+
81
+ def _score_term_streak(text: str, terms: List[str]) -> int:
82
+ """Consecutive-significant-term density bonus (e.g. 'clinical reasoning tutor')."""
83
+ if not terms or not text:
84
+ return 0
85
+ lowered = text.lower()
86
+ streak = 0
87
+ best = 0
88
+ words = re.findall(r"[a-z0-9]+", lowered)
89
+ term_set_low = {t.lower() for t in terms}
90
+ for w in words:
91
+ if w in term_set_low:
92
+ streak += 1
93
+ best = max(best, streak)
94
+ else:
95
+ streak = 0
96
+ return 2 * best + _term_hits(text, terms)
97
+
98
+
99
+ def _clean_readme(content: str) -> str:
100
+ """Strip YAML frontmatter + GitHub API boilerplate blockquote lines."""
101
+ if content.startswith("---"):
102
+ parts = content.split("---", 2)
103
+ if len(parts) >= 3:
104
+ content = parts[2]
105
+ out = []
106
+ for line in content.splitlines():
107
+ stripped = line.strip()
108
+ if _BOILERPLATE_LINE_RE.match(stripped):
109
+ continue
110
+ if stripped.startswith("> **GitHub Repository:") or stripped.startswith("> **Primary Language:") or stripped.startswith(">-"):
111
+ continue
112
+ out.append(line)
113
+ return "\n".join(out)
114
+
115
+
116
+ def _score_readme_sections(content: str, terms: List[str], max_chars: int = 1800) -> List[Tuple[str, str, int]]:
117
+ """Split a project README into heading-level chunks, scored by query hits.
118
+
119
+ Returns (heading, body, score) triples sorted best-first (score desc).
120
+ """
121
+ cleaned = _clean_readme(content)
122
+ sections: List[Tuple[str, str]] = []
123
+ cur_heading: Optional[str] = None
124
+ cur_body: List[str] = []
125
+
126
+ def _flush() -> None:
127
+ nonlocal cur_body
128
+ body = "\n".join(cur_body).strip()
129
+ cur_body = []
130
+ if body:
131
+ sections.append((cur_heading or "Overview", body))
132
+
133
+ for line in cleaned.splitlines():
134
+ if re.match(r"^#{1,4} ", line):
135
+ _flush()
136
+ cur_heading = re.sub(r"^#{1,4} ", "", line).strip()
137
+ else:
138
+ cur_body.append(line)
139
+ _flush()
140
+
141
+ scored = []
142
+ for heading, body in sections:
143
+ if not body:
144
+ continue
145
+ score = _term_score(heading, body, terms)
146
+ if score > 0:
147
+ truncated = body[:max_chars].rstrip()
148
+ if len(body) > max_chars:
149
+ truncated += "..."
150
+ scored.append((heading, truncated, score))
151
+ scored.sort(key=lambda x: x[2], reverse=True)
152
+ return scored
153
+
154
+
155
+ def _score_profile_sections(content: str, terms: List[str], max_chars: int = 1200) -> List[Tuple[str, str, int]]:
156
+ """Split a profile / background doc into heading-level chunks, scored."""
157
+ sections: List[Tuple[str, str]] = []
158
+ current_heading = ""
159
+ cur: List[str] = []
160
+
161
+ def _flush() -> None:
162
+ nonlocal cur
163
+ body = "\n".join(cur).strip()
164
+ cur = []
165
+ if body:
166
+ sections.append((current_heading, body))
167
+
168
+ for line in content.splitlines():
169
+ if re.match(r"^#{1,3} ", line):
170
+ _flush()
171
+ current_heading = line.lstrip("# ").strip()
172
+ elif line.strip() == "---":
173
+ _flush()
174
+ else:
175
+ cur.append(line)
176
+ _flush()
177
+
178
+ scored = []
179
+ for heading, body in sections:
180
+ if not body.strip():
181
+ continue
182
+ score = _score_term_streak(heading + "\n" + body, terms)
183
+ if score > 0:
184
+ truncated = body[:max_chars].rstrip()
185
+ if len(body) > max_chars:
186
+ truncated += "..."
187
+ scored.append((f"### {heading}", truncated, score))
188
+ scored.sort(key=lambda x: x[2], reverse=True)
189
+ return scored
190
+
191
+
192
+ def _clean_linkedin_posts(posts_text: str) -> List[str]:
193
+ """Strip YAML/meta boilerplate and without params, return bare post bodies."""
194
+ if posts_text.startswith("---"):
195
+ parts = posts_text.split("---", 2)
196
+ if len(parts) >= 3:
197
+ posts_text = parts[2]
198
+ posts = []
199
+ for block in re.split(r"(?m)^## ", posts_text):
200
+ block = block.strip()
201
+ if not block:
202
+ continue
203
+ lines: List[str] = []
204
+ for line in block.splitlines():
205
+ ls = line.strip()
206
+ if ls.startswith("**Date:") or ls.startswith("**Post Title") or ls.startswith("> **Link:"):
207
+ continue
208
+ lines.append(line)
209
+ body = "\n".join(lines).strip()
210
+ if body:
211
+ posts.append("# " + body)
212
+ return posts
213
+
214
+
215
+ class KnowledgeService:
216
+ """Single responsibility: every read/write against Arun's knowledge data."""
217
+
218
+ def __init__(self, root_dir: Optional[str] = None):
219
+ self.root_dir = root_dir or _project_root()
220
+ self.data_dir = os.path.join(self.root_dir, "data")
221
+
222
+ # ------------------------------------------------------------------ #
223
+ # Static persona documents (2-tuple used by the legacy API layer) #
224
+ # ------------------------------------------------------------------ #
225
+ def load_static_profile_and_rules(self) -> Tuple[str, str]:
226
+ profile_content = ""
227
+ rules_content = ""
228
+
229
+ profile_path = os.path.join(self.data_dir, "static", "public_profile.md")
230
+ rules_path = os.path.join(self.data_dir, "static", "rules_of_engagement.md")
231
+
232
+ if os.path.exists(profile_path):
233
+ with open(profile_path, "r", encoding="utf-8") as f:
234
+ profile_content = f.read()
235
+
236
+ if os.path.exists(rules_path):
237
+ with open(rules_path, "r", encoding="utf-8") as f:
238
+ rules_content = f.read()
239
+
240
+ return profile_content, rules_content
241
+
242
+ # ------------------------------------------------------------------ #
243
+ # GitHub live data
244
+ # ------------------------------------------------------------------ #
245
+ def fetch_live_github(self, username: str = "neural-arun") -> str:
246
+ try:
247
+ res = requests.get(
248
+ f"https://api.github.com/users/{username}/repos?sort=updated&per_page=10",
249
+ timeout=5,
250
+ )
251
+ if res.status_code == 200:
252
+ repos = res.json()
253
+ lines = [
254
+ f"β€’ [{r['name']}]({r['html_url']}) - {r.get('description', 'No description')} (Updated: {r['updated_at'][:10]})"
255
+ for r in repos
256
+ ]
257
+ return "### Live GitHub Repositories:\n" + "\n".join(lines)
258
+ except Exception as e:
259
+ return f"GitHub fetch error: {e}"
260
+ return "Could not fetch GitHub data."
261
+
262
+ # ------------------------------------------------------------------ #
263
+ # Free-form knowledge search (previously search_arun_knowledge) #
264
+ # ------------------------------------------------------------------ #
265
+ def search(self, query: str) -> str:
266
+ """Retrieve a compact, relevance-ranked set of knowledge chunks.
267
+
268
+ Every source (project READMEs, LinkedIn posts, static profile docs,
269
+ verified Q&A pairs) is split into heading-level chunks, the YAML/GitHub
270
+ boilerplate is stripped, each chunk is scored against the query's
271
+ significant terms, and only the best-scoring chunks are returned. The
272
+ tool output is therefore a bounded set of *clean sections* instead of
273
+ whole raw files.
274
+ """
275
+ data_dir = self.data_dir
276
+ cleaned_query = (query or "").lower().replace("-", " ").replace("_", " ")
277
+ query_terms = _query_terms(cleaned_query)
278
+
279
+ project_aliases = {
280
+ "med_coach": ["med_coach", "medcoach", "med coach", "medical tutor", "clinical reasoning", "clinical tutor"],
281
+ "legal_RAG_system": ["legal_rag_system", "legal rag", "legal", "ipc", "indian penal code", "chunking"],
282
+ "neet-bot": ["neet-bot", "neet bot", "neet 2027", "cbt simulator", "ncert", "mcq"],
283
+ "real_state_listing_scraper": ["real_state_listing_scraper", "99acres", "cloudflare", "scraper", "real estate"],
284
+ "ArunCore": ["aruncore", "profile", "assistant", "vector database", "reranking", "fastapi"],
285
+ }
286
+
287
+ matched_folders = []
288
+ for folder_name, synonyms in project_aliases.items():
289
+ if any(syn in cleaned_query for syn in synonyms):
290
+ matched_folders.append(folder_name)
291
+
292
+ results: List[Tuple[float, str]] = []
293
+
294
+ # 1. PROJECT READMES -> section-level chunks, ranked by relevance.
295
+ for folder in matched_folders:
296
+ target_readme = os.path.join(data_dir, "github", folder, "README.md")
297
+ if os.path.exists(target_readme):
298
+ with open(target_readme, "r", encoding="utf-8") as f:
299
+ content = f.read()
300
+ for heading, body, score in _score_readme_sections(content, query_terms):
301
+ if score > 0 and body:
302
+ block = f"## {heading}\n{body.strip()}"
303
+ results.append((_relevance(score), f"--- Project README ({folder}) ---\n{block}"))
304
+
305
+ stop_words = {"how", "does", "the", "a", "an", "is", "for", "to", "of", "with", "work", "what", "tell", "me", "about", "can", "you", "who", "where", "why", "arun"}
306
+ significant_words = [w.strip("?,.!") for w in cleaned_query.split() if w.strip("?,.!") not in stop_words and len(w) > 2]
307
+
308
+ # 2. LINKEDIN POSTS: strip meta boilerplate, chunk per post, rank.
309
+ posts_path = os.path.join(data_dir, "linkedin", "posts.md")
310
+ if os.path.exists(posts_path):
311
+ with open(posts_path, "r", encoding="utf-8") as f:
312
+ posts_text = f.read()
313
+ for post in _clean_linkedin_posts(posts_text):
314
+ body = post.strip()
315
+ if not body:
316
+ continue
317
+ score = sum(1 for w in significant_words if w.lower() in body.lower())
318
+ if score > 0 and query_terms:
319
+ results.append((_relevance(score, 0.8), f"--- Relevant LinkedIn Post ---\n{body[:2200]}"))
320
+
321
+ # 3. CORE PROFILE & BACKGROUND DOCS: heading-level chunks, ranked.
322
+ other_static_files = [
323
+ os.path.join(data_dir, "static", "public_profile.md"),
324
+ os.path.join(data_dir, "static", "rules_of_engagement.md"),
325
+ os.path.join(data_dir, "raw", "personal_background.md"),
326
+ ]
327
+ static_hits: List[Tuple[str, str, float]] = []
328
+ for s_file in other_static_files:
329
+ if os.path.exists(s_file):
330
+ with open(s_file, "r", encoding="utf-8") as f:
331
+ content = f.read()
332
+ for heading, chunk, score in _score_profile_sections(content, query_terms):
333
+ if score > 0:
334
+ block = f"{heading}\n{chunk.strip()}"
335
+ if len(block.strip()) > 80:
336
+ static_hits.append((_relevance(score, 0.9), f"--- Document Insight ({os.path.basename(s_file)}) ---\n{block.strip()}"))
337
+ results.extend(static_hits)
338
+
339
+ # Domain fallback: "stack/tech/language/framework/tools" queries that
340
+ # the lexical matcher missed still deserve the Technical Stack section.
341
+ stack_terms = {"stack", "tech", "technology", "technologies", "language", "framework", "tool", "tools", "languages"}
342
+ if static_hits and not any(
343
+ "Technical Stack" in block for _, block in static_hits
344
+ ) and any(t in stack_terms for t in query_terms):
345
+ profile_path = os.path.join(data_dir, "static", "public_profile.md")
346
+ if os.path.exists(profile_path):
347
+ with open(profile_path, "r", encoding="utf-8") as f:
348
+ profile_content = f.read()
349
+ for heading, chunk, score in _score_profile_sections(profile_content, stack_terms):
350
+ if "Technical Stack" in heading:
351
+ block = f"{heading}\n{chunk.strip()}"
352
+ results.append((2.0, f"--- Document Insight (public_profile.md) ---\n{block}"))
353
+
354
+ # 4. VERIFIED Q&A PAIRS from the active-learning store.
355
+ unknown_questions_path = os.path.join(data_dir, "raw", "unknown_questions.json")
356
+ if os.path.exists(unknown_questions_path):
357
+ try:
358
+ with open(unknown_questions_path, "r", encoding="utf-8") as f:
359
+ uq_data = json.load(f)
360
+ if isinstance(uq_data, list):
361
+ for item in uq_data:
362
+ q_item = item.get("question", "")
363
+ a_item = item.get("answer", "")
364
+ if any(w in q_item.lower() or w in a_item.lower() for w in significant_words):
365
+ results.append((0.6, f"--- Verified Q&A Pair ---\nQuestion: {q_item}\nAnswer: {a_item}"))
366
+ except Exception as e:
367
+ print(f"[UNKNOWN QUESTIONS READ ERROR] {e}")
368
+
369
+ # Sort by relevance score, dedupe, cap the total payload.
370
+ results.sort(key=lambda x: x[0], reverse=True)
371
+ unique_results = []
372
+ seen = set()
373
+ payload_len = 0
374
+ for _, block in results:
375
+ block = block.strip()
376
+ if block in seen:
377
+ continue
378
+ if len(block) > 3000:
379
+ block = block[:3000].rstrip() + "..."
380
+ if payload_len + len(block) > 9000:
381
+ break
382
+ unique_results.append(block)
383
+ seen.add(block)
384
+ payload_len += len(block) + 2
385
+
386
+ if unique_results:
387
+ return "\n\n".join(unique_results)
388
+
389
+ # Run out of chunks -> auto-trigger notification
390
+ schedule_notify_arun(
391
+ "UNKNOWN_QUESTION",
392
+ f"Unknown Question (No KB Match): {query}",
393
+ )
394
+ return (
395
+ "No exact match found in knowledge base. Auto-triggered UNKNOWN_QUESTION alert to Arun's phone. "
396
+ "YOU MUST CALL notify_arun AND ASK THE USER FOR THEIR CONTACT INFO (Name, Email, Phone/WhatsApp)."
397
+ )
398
+
399
+ # ------------------------------------------------------------------ #
400
+ # Active learning: persist a verified Q&A and re-ingest into memory #
401
+ # ------------------------------------------------------------------ #
402
+ def save_verified_answer(self, question: str, answer: str) -> str:
403
+ """Writes an answered unknown question into data/raw/unknown_questions.json and triggers vector DB re-ingestion."""
404
+ target_file = os.path.join(self.data_dir, "raw", "unknown_questions.json")
405
+ os.makedirs(os.path.dirname(target_file), exist_ok=True)
406
+
407
+ now_iso = datetime.datetime.utcnow().isoformat() + "Z"
408
+
409
+ entry = {
410
+ "question": question.strip(),
411
+ "answer": answer.strip(),
412
+ "timestamp": now_iso,
413
+ }
414
+
415
+ existing = []
416
+ if os.path.exists(target_file):
417
+ try:
418
+ with open(target_file, "r", encoding="utf-8") as f:
419
+ existing = json.load(f)
420
+ if not isinstance(existing, list):
421
+ existing = []
422
+ except Exception:
423
+ existing = []
424
+
425
+ updated = False
426
+ for item in existing:
427
+ if item.get("question", "").lower() == question.strip().lower():
428
+ item["answer"] = answer.strip()
429
+ item["timestamp"] = now_iso
430
+ updated = True
431
+ break
432
+
433
+ if not updated:
434
+ existing.append(entry)
435
+
436
+ with open(target_file, "w", encoding="utf-8") as f:
437
+ json.dump(existing, f, indent=2)
438
+
439
+ ingest_script = os.path.join(self.root_dir, "backend", "app", "core", "ingest.py")
440
+ try:
441
+ subprocess.Popen(["python3", ingest_script])
442
+ except Exception as e:
443
+ print(f"[REINGEST ERROR] {e}")
444
+
445
+ return "SUCCESS: Saved to unknown_questions.json and ingested into memory."
446
+
447
+
448
+ knowledge_service = KnowledgeService()
449
+
450
+
451
+ def load_static_context() -> Tuple[str, str]:
452
+ """Backward-compatible 2-tuple reader (profile, rules)."""
453
+ return knowledge_service.load_static_profile_and_rules()
backend/app/services/memory_manager.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rolling conversation memory with summary-compression.
2
+
3
+ A single session's chat lives in a shrinking window. Every N turns the oldest
4
+ messages are folded into a compact running summary by the summary LLM, so long
5
+ conversations keep full context without blowing the token budget.
6
+ """
7
+ from typing import Any, List, Optional
8
+
9
+ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
10
+
11
+
12
+ class RollingMemory:
13
+ """Stateful per-session memory used across API and Telegram chat loops."""
14
+
15
+ def __init__(self, summary_llm: Any, max_turns: int = 4):
16
+ self.summary_llm = summary_llm
17
+ self.max_turns = max_turns
18
+ self.history: List[BaseMessage] = []
19
+ self.running_summary: str = "No prior summary. This is the start of the conversation."
20
+ self.invocation_count = 0
21
+
22
+ def add_interaction(self, human_text: str, ai_text: str):
23
+ self.history.append(HumanMessage(content=human_text))
24
+ self.history.append(AIMessage(content=ai_text))
25
+ self.invocation_count += 1
26
+
27
+ if self.invocation_count >= self.max_turns:
28
+ self._summarize_and_prune()
29
+
30
+ def _summarize_and_prune(self):
31
+ print("\n[SYSTEM] Triggering background summarization...")
32
+ messages_to_summarize = self.history[:-4]
33
+
34
+ if not messages_to_summarize:
35
+ return
36
+
37
+ chat_transcript = "\n".join(
38
+ [f"{'User' if isinstance(m, HumanMessage) else 'Arun Assistant'}: {m.content}" for m in messages_to_summarize]
39
+ )
40
+
41
+ prompt = (
42
+ "You are an internal memory compression engine for Arun's AI Assistant.\n"
43
+ "Merge the existing summary with the new transcript. Preserve technical context, names, project mentions, user goals, and important decisions. "
44
+ "Keep it concise and stable. Return no more than 5 sentences.\n\n"
45
+ f"--- EXISTING SUMMARY ---\n{self.running_summary}\n\n"
46
+ f"--- NEW CHAT TO MERGE ---\n{chat_transcript}"
47
+ )
48
+
49
+ try:
50
+ res = self.summary_llm.invoke([SystemMessage(content=prompt)])
51
+ self.running_summary = res.content.strip()
52
+ self.history = self.history[-4:]
53
+ self.invocation_count = len(self.history) // 2
54
+ print(f"[SYSTEM] Memory compressed. New summary: {self.running_summary[:120]}...")
55
+ except Exception as e:
56
+ print(f"[SYSTEM ERROR] Failed to summarize memory: {e}")
57
+
58
+ def get_messages(self) -> List[BaseMessage]:
59
+ return self.history
60
+
61
+ def clear(self):
62
+ self.history.clear()
63
+ self.running_summary = "No prior summary. This is the start of the conversation."
64
+ self.invocation_count = 0
65
+
66
+
67
+ # Backwards-compatible alias: the decoupled API exposes this class as
68
+ # `MemoryManager` while the agent layer keeps the historical `RollingMemory` name.
69
+ MemoryManager = RollingMemory
backend/app/services/notification_service.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Telegram notification & alert delivery.
2
+
3
+ All outgoing Telegram traffic is consolidated here: chat history logging,
4
+ debug events, per-message live chat alerts with the 1-Click Join link, and
5
+ the category-based notify_arun escalation system. Every call is fire-and-forget
6
+ via the shared background queue so the chat loop never blocks on the network.
7
+ """
8
+ import os
9
+ import json
10
+ import time
11
+ from typing import Dict, Any, List, Optional, Tuple
12
+
13
+ from backend.app.services.background import submit_background_task
14
+ from backend.app.services.auth_service import generate_admin_token
15
+
16
+
17
+ def _is_truthy_env(val: Optional[str], default: bool = True) -> bool:
18
+ if val is None:
19
+ return default
20
+ return val.strip().lower() in ("1", "true", "yes", "on")
21
+
22
+
23
+ def _is_telegram_debug_enabled() -> bool:
24
+ return _is_truthy_env(os.getenv("TELEGRAM_DEBUG_ENABLED"), default=True)
25
+
26
+
27
+ def _get_telegram_target(debug: bool = False, alert: bool = False) -> Tuple[Optional[str], Optional[str]]:
28
+ if alert:
29
+ token = os.getenv("TELEGRAM_ALERT_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
30
+ chat_id = os.getenv("TELEGRAM_ALERT_CHAT_ID") or os.getenv("TELEGRAM_CHAT_ID")
31
+ return token, chat_id
32
+ if debug:
33
+ token = os.getenv("TELEGRAM_DEBUG_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
34
+ chat_id = os.getenv("TELEGRAM_DEBUG_CHAT_ID") or os.getenv("TELEGRAM_CHAT_ID")
35
+ return token, chat_id
36
+
37
+ return os.getenv("TELEGRAM_BOT_TOKEN"), os.getenv("TELEGRAM_CHAT_ID")
38
+
39
+
40
+ def safe_truncate(text: str, limit: int = 1500) -> str:
41
+ cleaned = (text or "").strip()
42
+ if len(cleaned) <= limit:
43
+ return cleaned
44
+ return cleaned[:limit] + "..."
45
+
46
+
47
+ def _escape_html(text: str) -> str:
48
+ return (
49
+ (text or "")
50
+ .replace("&", "&amp;")
51
+ .replace("<", "&lt;")
52
+ .replace(">", "&gt;")
53
+ )
54
+
55
+
56
+ def _chunk_text(text: str, limit: int = 2200) -> List[str]:
57
+ cleaned = (text or "").strip() or "(empty)"
58
+ parts: List[str] = []
59
+ remaining = cleaned
60
+
61
+ while len(remaining) > limit:
62
+ split_at = remaining.rfind("\n", 0, limit)
63
+ if split_at < int(limit * 0.5):
64
+ split_at = remaining.rfind(" ", 0, limit)
65
+ if split_at <= 0:
66
+ split_at = limit
67
+
68
+ parts.append(remaining[:split_at].strip())
69
+ remaining = remaining[split_at:].lstrip()
70
+
71
+ if remaining:
72
+ parts.append(remaining)
73
+
74
+ return parts or ["(empty)"]
75
+
76
+
77
+ TELEGRAM_DELIVERY_LOGS: List[Dict[str, Any]] = []
78
+
79
+
80
+ def _record_telegram_log(label: str, status: str, detail: str) -> None:
81
+ entry = {
82
+ "time": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
83
+ "label": label,
84
+ "status": status,
85
+ "detail": detail,
86
+ }
87
+ TELEGRAM_DELIVERY_LOGS.append(entry)
88
+ if len(TELEGRAM_DELIVERY_LOGS) > 30:
89
+ TELEGRAM_DELIVERY_LOGS.pop(0)
90
+
91
+
92
+ def _send_telegram_message(
93
+ token: str,
94
+ chat_id: str,
95
+ text: str,
96
+ parse_mode: str = "HTML",
97
+ max_attempts: int = 3,
98
+ delivery_label: str = "default",
99
+ retry_sleep_seconds: float = 1.0,
100
+ ) -> str:
101
+ import urllib.request
102
+ import urllib.parse
103
+ import ssl
104
+
105
+ token = (token or "").strip()
106
+ if token.lower().startswith("bot"):
107
+ token = token[3:].strip()
108
+
109
+ chat_id = (chat_id or "").strip()
110
+
111
+ if not token or not chat_id:
112
+ msg = f"FAILED: Token or Chat ID empty (token_len={len(token)}, chat_id_len={len(chat_id)})"
113
+ _record_telegram_log(delivery_label, "MISSING_CREDS", msg)
114
+ print(f"[TELEGRAM:{delivery_label}] {msg}")
115
+ return msg
116
+
117
+ chunks = _chunk_text(text)
118
+ total_chunks = len(chunks)
119
+
120
+ ssl_ctx = ssl.create_default_context()
121
+ ssl_ctx.check_hostname = False
122
+ ssl_ctx.verify_mode = ssl.CERT_NONE
123
+
124
+ for idx, chunk in enumerate(chunks, 1):
125
+ payload: Dict[str, Any] = {
126
+ "token": token,
127
+ "chat_id": chat_id,
128
+ "text": chunk,
129
+ "disable_web_page_preview": True,
130
+ }
131
+ if parse_mode:
132
+ payload["parse_mode"] = parse_mode
133
+
134
+ sent_chunk = False
135
+ last_error = ""
136
+
137
+ # Attempt 1: Try Vercel Serverless Relay (bypasses HF Space Telegram firewall block)
138
+ relay_url = "https://aruncore.vercel.app/api/telegram"
139
+ try:
140
+ relay_data = json.dumps(payload).encode("utf-8")
141
+ relay_req = urllib.request.Request(
142
+ relay_url,
143
+ data=relay_data,
144
+ headers={"Content-Type": "application/json", "User-Agent": "ArunCore/1.0", "Connection": "close"},
145
+ )
146
+ with urllib.request.urlopen(relay_req, timeout=8, context=ssl_ctx) as resp:
147
+ resp_bytes = resp.read()
148
+ resp_data = json.loads(resp_bytes.decode("utf-8"))
149
+ if resp.status == 200 and resp_data.get("ok"):
150
+ sent_chunk = True
151
+ except Exception as e:
152
+ last_error = f"Vercel Relay error: {e}"
153
+
154
+ if not sent_chunk:
155
+ # Direct Telegram API Fallback
156
+ for attempt in range(1, max_attempts + 1):
157
+ try:
158
+ url = f"https://api.telegram.org/bot{token}/sendMessage"
159
+ direct_payload = {
160
+ "chat_id": chat_id,
161
+ "text": chunk,
162
+ "disable_web_page_preview": True,
163
+ }
164
+ if parse_mode:
165
+ direct_payload["parse_mode"] = parse_mode
166
+
167
+ data = json.dumps(direct_payload).encode("utf-8")
168
+ req = urllib.request.Request(
169
+ url,
170
+ data=data,
171
+ headers={
172
+ "Content-Type": "application/json",
173
+ "User-Agent": "ArunCore/1.0",
174
+ "Connection": "close",
175
+ },
176
+ )
177
+ with urllib.request.urlopen(req, timeout=10, context=ssl_ctx) as resp:
178
+ resp_bytes = resp.read()
179
+ resp_data = json.loads(resp_bytes.decode("utf-8"))
180
+ if resp.status == 200 and resp_data.get("ok"):
181
+ sent_chunk = True
182
+ break
183
+ else:
184
+ last_error = f"HTTP {resp.status}: {resp_bytes.decode('utf-8')[:200]}"
185
+ except Exception as e:
186
+ last_error = str(e)
187
+
188
+ if attempt < max_attempts:
189
+ time.sleep(retry_sleep_seconds)
190
+
191
+ if not sent_chunk and parse_mode == "HTML":
192
+ fallback_payload = {
193
+ "chat_id": chat_id,
194
+ "text": f"[{delivery_label}] (Plain Text Fallback {idx}/{total_chunks})\n{chunk}",
195
+ "disable_web_page_preview": True,
196
+ }
197
+ try:
198
+ url = f"https://api.telegram.org/bot{token}/sendMessage"
199
+ data = json.dumps(fallback_payload).encode("utf-8")
200
+ req = urllib.request.Request(
201
+ url,
202
+ data=data,
203
+ headers={
204
+ "Content-Type": "application/json",
205
+ "User-Agent": "ArunCore/1.0",
206
+ "Connection": "close",
207
+ },
208
+ )
209
+ with urllib.request.urlopen(req, timeout=12, context=ssl_ctx) as resp:
210
+ resp_bytes = resp.read()
211
+ resp_data = json.loads(resp_bytes.decode("utf-8"))
212
+ if resp.status == 200 and resp_data.get("ok"):
213
+ sent_chunk = True
214
+ except Exception as e:
215
+ last_error = f"Fallback error: {e}"
216
+
217
+ if not sent_chunk:
218
+ err_msg = f"FAILED chunk {idx}/{total_chunks}: {last_error}"
219
+ print(f"[TELEGRAM:{delivery_label}] {err_msg}")
220
+ _record_telegram_log(delivery_label, "ERROR", err_msg)
221
+ return err_msg
222
+
223
+ print(f"[TELEGRAM:{delivery_label}] SUCCESS")
224
+ _record_telegram_log(delivery_label, "SUCCESS", f"Delivered {total_chunks} chunk(s)")
225
+ return "SUCCESS: message delivered."
226
+
227
+
228
+ def send_chat_history_to_telegram(
229
+ session_id: str,
230
+ user_input: str,
231
+ assistant_response: str,
232
+ thoughts: Optional[List[str]] = None,
233
+ tool_calls: Optional[List[str]] = None,
234
+ retrieved_chunks: Optional[List[str]] = None,
235
+ github_data: Optional[List[str]] = None,
236
+ ) -> str:
237
+ token, chat_id = _get_telegram_target(debug=False)
238
+ if not token or not chat_id:
239
+ return "FAILED: Telegram credentials missing."
240
+
241
+ clean_user = _escape_html(safe_truncate(user_input, 1500))
242
+ clean_ai = _escape_html(safe_truncate(assistant_response, 2500))
243
+
244
+ tools_html = ""
245
+ if tool_calls and len(tool_calls) > 0:
246
+ tools_str = "\n".join([f"β€’ <code>{_escape_html(safe_truncate(t, 250))}</code>" for t in tool_calls])
247
+ tools_html = f"\n\n<b>🧠 AI Decisions & Tool Calls:</b>\n{tools_str}"
248
+
249
+ chunks_html = ""
250
+ if retrieved_chunks and len(retrieved_chunks) > 0:
251
+ chunks_str = "\n".join([f"--- Chunk {i+1} ---\n{_escape_html(safe_truncate(c, 500))}" for i, c in enumerate(retrieved_chunks[:3])])
252
+ chunks_html = f"\n\n<b>πŸ“š RAG Knowledge Chunks Retrieved:</b>\n<code>{chunks_str}</code>"
253
+
254
+ github_html = ""
255
+ if github_data and len(github_data) > 0:
256
+ github_str = "\n".join([f"--- Repo Data ---\n{_escape_html(safe_truncate(g, 500))}" for g in github_data[:2]])
257
+ github_html = f"\n\n<b>πŸ™ Live GitHub Repositories Fetched:</b>\n<code>{github_str}</code>"
258
+
259
+ thoughts_html = ""
260
+ if thoughts and len(thoughts) > 0:
261
+ thoughts_str = "\n".join([f"β€’ {_escape_html(t)}" for t in thoughts])
262
+ thoughts_html = f"\n\n<b>βš™οΈ Execution Steps:</b>\n{thoughts_str}"
263
+
264
+ html = (
265
+ f"<b>πŸ“Š ARUNCORE FULL EXECUTION TRACE</b>\n"
266
+ f"<b>Session ID:</b> <code>{_escape_html(session_id)}</code>\n\n"
267
+ f"<b>πŸ‘€ User Question:</b>\n{clean_user}"
268
+ f"{tools_html}"
269
+ f"{chunks_html}"
270
+ f"{github_html}"
271
+ f"{thoughts_html}\n\n"
272
+ f"<b>πŸ€– AI Twin Final Reply:</b>\n{clean_ai}"
273
+ )
274
+
275
+ return _send_telegram_message(
276
+ token=token,
277
+ chat_id=chat_id,
278
+ text=html,
279
+ parse_mode="HTML",
280
+ delivery_label="chat_log",
281
+ )
282
+
283
+
284
+ def queue_chat_history_to_telegram(
285
+ session_id: str,
286
+ user_input: str,
287
+ assistant_response: str,
288
+ thoughts: Optional[List[str]] = None,
289
+ tool_calls: Optional[List[str]] = None,
290
+ retrieved_chunks: Optional[List[str]] = None,
291
+ github_data: Optional[List[str]] = None,
292
+ ) -> str:
293
+ submit_background_task(
294
+ "chat_history_log",
295
+ send_chat_history_to_telegram,
296
+ session_id,
297
+ user_input,
298
+ assistant_response,
299
+ thoughts or [],
300
+ tool_calls or [],
301
+ retrieved_chunks or [],
302
+ github_data or [],
303
+ )
304
+ return "QUEUED: chat history scheduled."
305
+
306
+
307
+ def send_debug_event_to_telegram(
308
+ event_type: str,
309
+ payload_summary: str,
310
+ metadata: Optional[Dict[str, Any]] = None,
311
+ ) -> str:
312
+ if not _is_telegram_debug_enabled():
313
+ return "SKIPPED: debug disabled."
314
+
315
+ token, chat_id = _get_telegram_target(debug=True)
316
+ if not token or not chat_id:
317
+ return "FAILED: Telegram debug credentials missing."
318
+
319
+ clean_event = _escape_html(event_type.upper())
320
+ clean_payload = _escape_html(safe_truncate(payload_summary, 2000))
321
+
322
+ meta_str = ""
323
+ if metadata:
324
+ meta_str = "\n".join([f"β€’ <b>{_escape_html(str(k))}:</b> {_escape_html(str(v))}" for k, v in metadata.items()])
325
+
326
+ html = f"<b>πŸ”§ DEBUG EVENT: {clean_event}</b>\n{meta_str}\n\n<code>{clean_payload}</code>"
327
+
328
+ return _send_telegram_message(
329
+ token=token,
330
+ chat_id=chat_id,
331
+ text=html,
332
+ parse_mode="HTML",
333
+ delivery_label="debug_event",
334
+ )
335
+
336
+
337
+ def queue_debug_event(
338
+ event_type: str,
339
+ payload_summary: str,
340
+ metadata: Optional[Dict[str, Any]] = None,
341
+ ) -> None:
342
+ submit_background_task(
343
+ "debug_event",
344
+ send_debug_event_to_telegram,
345
+ event_type,
346
+ payload_summary,
347
+ metadata,
348
+ )
349
+
350
+
351
+ def send_automated_chat_alert(
352
+ session_id: str,
353
+ user_input: str,
354
+ assistant_response: str,
355
+ ) -> str:
356
+ token, chat_id = _get_telegram_target(alert=True)
357
+ if not token or not chat_id:
358
+ return "FAILED: Telegram credentials missing."
359
+
360
+ admin_tok = generate_admin_token(session_id)
361
+ join_link = f"https://aruncore.vercel.app/?session_id={session_id}&admin_token={admin_tok}"
362
+
363
+ clean_user = _escape_html(safe_truncate(user_input, 1000))
364
+ clean_ai = _escape_html(safe_truncate(assistant_response, 1500))
365
+
366
+ html = (
367
+ f"🚨 <b>LIVE WEBSITE CHAT ACTIVITY</b>\n"
368
+ f"<b>Session ID:</b> <code>{_escape_html(session_id)}</code>\n\n"
369
+ f"<b>πŸ‘€ User Question:</b>\n{clean_user}\n\n"
370
+ f"<b>πŸ€– AI Response:</b>\n{clean_ai}\n\n"
371
+ f"πŸ”— <b><a href=\"{join_link}\">πŸ‘‰ CLICK HERE TO JOIN LIVE CHAT AS REAL ARUN</a></b>"
372
+ )
373
+
374
+ return _send_telegram_message(
375
+ token=token,
376
+ chat_id=chat_id,
377
+ text=html,
378
+ parse_mode="HTML",
379
+ delivery_label="every_chat_alert",
380
+ )
381
+
382
+
383
+ def queue_automated_chat_alert(session_id: str, user_input: str, assistant_response: str) -> None:
384
+ submit_background_task(
385
+ "automated_chat_alert_bg",
386
+ send_automated_chat_alert,
387
+ session_id,
388
+ user_input,
389
+ assistant_response,
390
+ )
391
+
392
+
393
+ def queue_maybe_notify_arun(
394
+ user_input: str,
395
+ reason: str = "",
396
+ channel: str = "api",
397
+ session_id: str = "",
398
+ final_response: str = "",
399
+ scratchpad: Optional[list] = None,
400
+ tool_map: Optional[dict] = None,
401
+ user_metadata: Optional[dict] = None,
402
+ pre_notified: bool = False,
403
+ ) -> None:
404
+ """Escalation hook: schedules an URGENT notify when contact intent is detected."""
405
+ submit_background_task(
406
+ "maybe_notify_arun",
407
+ _deliver_notify_arun,
408
+ "URGENT",
409
+ f"Reason: {reason or 'General check'}\nQuery: {user_input}",
410
+ )
411
+
412
+
413
+ _RECENT_ALERTS: Dict[str, float] = {}
414
+ _ALERT_DEDUP_WINDOW_SECONDS = 10.0
415
+
416
+
417
+ def _should_send_alert(category: str, user_input: str) -> bool:
418
+ now = time.time()
419
+
420
+ expired_keys = [k for k, timestamp in _RECENT_ALERTS.items() if now - timestamp > _ALERT_DEDUP_WINDOW_SECONDS]
421
+ for k in expired_keys:
422
+ _RECENT_ALERTS.pop(k, None)
423
+
424
+ key = f"{category.upper()}:{user_input.strip().lower()}"
425
+ if key in _RECENT_ALERTS:
426
+ return False
427
+
428
+ _RECENT_ALERTS[key] = now
429
+ return True
430
+
431
+
432
+ ALLOWED_NOTIFY_CATEGORIES = {
433
+ "UNKNOWN_QUESTION",
434
+ "URGENT",
435
+ "FEEDBACK",
436
+ "SYSTEM_ALERT",
437
+ "LEAD",
438
+ "ABUSE",
439
+ "WEIRD",
440
+ "SUSPICIOUS",
441
+ "OFF_TOPIC",
442
+ }
443
+
444
+ _CATEGORY_HEADERS = {
445
+ "UNKNOWN_QUESTION": "🀷 UNKNOWN QUESTION",
446
+ "LEAD": "πŸ’Ό NEW LEAD / HIRING INQUIRY",
447
+ "URGENT": "🚨 URGENT ALERT",
448
+ "ABUSE": "🀬 ABUSE / RUDE MESSAGE",
449
+ "WEIRD": "πŸ‘€ WEIRD / UNUSUAL MESSAGE",
450
+ "SUSPICIOUS": "πŸ•΅οΈ SUSPICIOUS ACTIVITY",
451
+ "OFF_TOPIC": "🚫 OFF-TOPIC / IRRELEVANT",
452
+ "FEEDBACK": "πŸ’¬ FEEDBACK",
453
+ "SYSTEM_ALERT": "βš™οΈ SYSTEM ALERT",
454
+ }
455
+
456
+
457
+ def _deliver_notify_arun(
458
+ category: str,
459
+ user_input: str,
460
+ user_metadata_json: str = "",
461
+ _fast: bool = False,
462
+ ) -> str:
463
+ token, chat_id = _get_telegram_target(alert=True)
464
+
465
+ if not token or not chat_id:
466
+ msg = "FAILED: Telegram credentials missing."
467
+ _record_telegram_log("notify_arun_bg", "MISSING_CREDS", msg)
468
+ return msg
469
+
470
+ category = (category or "UNKNOWN_QUESTION").strip().upper()
471
+ if category not in ALLOWED_NOTIFY_CATEGORIES:
472
+ category = "UNKNOWN_QUESTION"
473
+
474
+ cleaned_input = safe_truncate(user_input, 1200)
475
+
476
+ if not _should_send_alert(category, cleaned_input):
477
+ msg = f"SKIPPED: duplicate {category} alert suppressed within 10s window."
478
+ _record_telegram_log("notify_arun_bg", "SKIPPED_DEDUP", msg)
479
+ return msg
480
+
481
+ header = _CATEGORY_HEADERS.get(category, f"🚨 ALERT: {category}")
482
+ html = (
483
+ f"<b>{header}</b>\n\n"
484
+ f"<b>User Query / Details:</b>\n{_escape_html(cleaned_input)}\n\n"
485
+ f"<b>Category:</b> {category}\n"
486
+ f"<b>Contact:</b> +91 8881109193 | neural.arun.dev@gmail.com\n\n"
487
+ f"<i>πŸ’‘ Reply directly to this message to save your answer into AI memory!</i>"
488
+ )
489
+
490
+ return _send_telegram_message(
491
+ token=token,
492
+ chat_id=chat_id,
493
+ text=html,
494
+ parse_mode="HTML",
495
+ delivery_label="notify_arun_bg",
496
+ )
497
+
498
+
499
+ def schedule_notify_arun(category: str, user_input: str, user_metadata_json: str = "") -> str:
500
+ """Queue an urgent notify_arun Telegram alert (used by the agent tool)."""
501
+ submit_background_task(
502
+ "notify_arun_bg",
503
+ _deliver_notify_arun,
504
+ category,
505
+ user_input,
506
+ user_metadata_json or "",
507
+ )
508
+ return "QUEUED: notify_arun alert scheduled."
backend/app/services/prompt_builder.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """System prompt assembly for the ArunCore agent.
2
+
3
+ Every tenant-facing prompt (both the legacy demos dictionary and the split
4
+ tenant configs) flows through here. The builder returns the exact same prompt
5
+ templates the legacy monolith produced, so prompt behavior is unchanged while
6
+ the assembly itself lives in a single-responsibility service.
7
+ """
8
+ import os
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
12
+ from langchain_core.messages import SystemMessage
13
+
14
+ from backend.app.services.tenant_service import TenantService
15
+
16
+
17
+ def _backend_app_dir() -> str:
18
+ """backend/app/ (parent of services/, core/, prompts/, ...)."""
19
+ return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20
+
21
+
22
+ def _project_root() -> str:
23
+ """profile/ (repository root)."""
24
+ return os.path.dirname(_backend_app_dir())
25
+
26
+
27
+ def load_static_context() -> Tuple[str, str, str, str, str]:
28
+ """Returns (system_prompt, guardrails, handoff, profile, rules).
29
+
30
+ The prompts/ directory may be absent on a fresh checkout; every missing
31
+ file simply resolves to an empty string, matching the legacy loader.
32
+ """
33
+ backend_app = _backend_app_dir()
34
+ root_dir = _project_root()
35
+
36
+ sys_prompt_path = os.path.join(backend_app, "prompts", "system_prompt.md")
37
+ guardrails_path = os.path.join(backend_app, "prompts", "guardrails.md")
38
+ handoff_path = os.path.join(backend_app, "prompts", "handoff_prompt.md")
39
+
40
+ profile_path = os.path.join(root_dir, "data", "static", "public_profile.md")
41
+ rules_path = os.path.join(root_dir, "data", "static", "rules_of_engagement.md")
42
+
43
+ def _read(path: str) -> str:
44
+ if os.path.exists(path):
45
+ with open(path, "r", encoding="utf-8") as f:
46
+ return f.read()
47
+ return ""
48
+
49
+ return (
50
+ _read(sys_prompt_path),
51
+ _read(guardrails_path),
52
+ _read(handoff_path),
53
+ _read(profile_path),
54
+ _read(rules_path),
55
+ )
56
+
57
+
58
+ class PromptBuilder:
59
+ """Constructs the final system prompt + chat template for an agent run."""
60
+
61
+ def __init__(self, tenant_service_: Optional[TenantService] = None):
62
+ self.tenant_service = tenant_service_ or TenantService()
63
+
64
+ def build_system_prompt(self, tutor_id: Optional[str] = None) -> str:
65
+ """Assembles the system prompt for either a tenant (demos dict) or the
66
+ default Arun twin persona. Contains a literal ``{running_summary}``
67
+ placeholder that the chat template fills per message."""
68
+ tutor_cfg = self.tenant_service.load_legacy_tutor_config(tutor_id)
69
+
70
+ if tutor_cfg:
71
+ backend_cfg = tutor_cfg.get("backend_llm_configuration", {})
72
+ sys_prompt_cfg = backend_cfg.get("system_prompt", {})
73
+
74
+ custom_prompt = sys_prompt_cfg.get("persona_identity") or tutor_cfg.get("custom_system_prompt") or "You are an AI Course Advisor."
75
+ rules_list = sys_prompt_cfg.get("project_guidelines") or tutor_cfg.get("custom_rules") or []
76
+ courses_data = tutor_cfg.get("courses") or []
77
+ about_bio = tutor_cfg.get("frontend_ui_dictionary", {}).get("about_view", {}).get("bio_paragraphs") or tutor_cfg.get("about_text") or ""
78
+
79
+ rules_txt = "\n".join([f"- {r}" for r in rules_list])
80
+ courses_txt = json_dumps_safe(courses_data)
81
+ about_txt = "\n".join(about_bio) if isinstance(about_bio, list) else str(about_bio)
82
+
83
+ return f"""
84
+ {custom_prompt}
85
+
86
+ --- INSTRUCTOR & COURSE KNOWLEDGE ---
87
+ About Instructor:
88
+ {about_txt}
89
+
90
+ Available Courses & Cohorts:
91
+ {courses_txt}
92
+
93
+ --- SPECIAL INSTRUCTIONS & RULES ---
94
+ {rules_txt}
95
+
96
+ --- LANGUAGE RULES ---
97
+ Always respond in the exact language used by the student (English or natural Hinglish).
98
+
99
+ --- PAST CONVERSATION SUMMARY ---
100
+ {{running_summary}}
101
+ """
102
+
103
+ sys_content, guard_content, handoff_content, profile, rules = load_static_context()
104
+
105
+ return f"""
106
+ --- MASTER RULES OF ENGAGEMENT & PERSONA (PRIMARY CORE) ---
107
+ {rules}
108
+
109
+ --- IDENTITY PROFILE & TECHNICAL SPECIFICATIONS ---
110
+ {profile}
111
+
112
+ --- GUARDRAILS & STEERING RULES ---
113
+ {guard_content}
114
+
115
+ --- 3-WAY LIVE CHAT & HANDOFF RULES ---
116
+ {handoff_content}
117
+
118
+ --- PAST CONVERSATION SUMMARY ---
119
+ {{running_summary}}
120
+ """
121
+
122
+ @staticmethod
123
+ def build_chat_prompt(system_prompt: str) -> ChatPromptTemplate:
124
+ return ChatPromptTemplate.from_messages(
125
+ [
126
+ ("system", system_prompt),
127
+ MessagesPlaceholder(variable_name="chat_history"),
128
+ ("human", "{input}"),
129
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
130
+ ]
131
+ )
132
+
133
+ @staticmethod
134
+ def inject_live_human_notice(messages: List[Any], arun_human_msgs: List[Dict[str, Any]]) -> List[Any]:
135
+ """Inserts the 3-way live chat presence notice when the real human is active."""
136
+ if not arun_human_msgs:
137
+ return messages
138
+
139
+ formatted_msgs = "\n".join([f"β€’ Real Human Instructor (οΏ½οΏ½β€πŸ’»): \"{m.get('text')}\"" for m in arun_human_msgs])
140
+ live_notice = SystemMessage(content=(
141
+ f"🟒 CRITICAL LIVE 3-WAY CHAT NOTICE (REAL HUMAN INSTRUCTOR IS PRESENT):\n"
142
+ f"The REAL HUMAN INSTRUCTOR (πŸ‘¨β€πŸ’») HAS JOINED THIS CHAT ROOM LIVE AND IS CURRENTLY CHATTING!\n\n"
143
+ f"REAL INSTRUCTOR'S MESSAGES IN THIS SESSION:\n{formatted_msgs}\n\n"
144
+ f"MANDATORY INSTRUCTIONS FOR AI ASSISTANT IN THIS 3-WAY CHAT:\n"
145
+ f"1. Acknowledge that the REAL human instructor is present right next to you in this chat session!\n"
146
+ f"2. If the user asks how the instructor came here or questions about their arrival, explain enthusiastically: \"The real instructor tapped their 1-Click Telegram link and joined our chat live from their phone! So both of us (Real Instructor + AI Assistant) are here together with you!\"\n"
147
+ f"3. Never confuse yourself as the human β€” you are the AI Assistant co-piloting alongside the Real Instructor!"
148
+ ))
149
+
150
+ new_messages = list(messages)
151
+ if len(new_messages) > 1:
152
+ new_messages.insert(1, live_notice)
153
+ else:
154
+ new_messages.append(live_notice)
155
+ return new_messages
156
+
157
+
158
+ def json_dumps_safe(obj: Any) -> str:
159
+ """dump with braces escaped so the LLM output isn't treated as prompt vars."""
160
+ import json
161
+
162
+ return json.dumps(obj, indent=2).replace("{", "{{").replace("}", "}}")
backend/app/services/rag_service.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hybrid RAG coordinator.
2
+
3
+ Composes the knowledge-retrieval pipeline for a tenant: dense vector search
4
+ (ChromaDB), sparse keyword search (BM25), and reranking get their turn here
5
+ in future integration; today ChromaDB is built at ingest time and runtime
6
+ retrieval is served by `KnowledgeService` (project READMEs + LinkedIn posts +
7
+ profile sections + verified Q&A pairs). This service also doubles as the
8
+ active-learning ingestion point for owner-verified answers.
9
+ """
10
+ import hashlib
11
+ from typing import List, Dict, Any
12
+
13
+ from backend.app.services.knowledge_service import knowledge_service
14
+
15
+
16
+ class RAGService:
17
+ """Hybrid RAG coordinator: retrieval + active-learning ingestion per tenant."""
18
+
19
+ def __init__(self, tenant_id: str = "arun"):
20
+ self.tenant_id = tenant_id
21
+
22
+ def retrieve_context(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
23
+ """Executes high-precision hybrid retrieval for the specified tenant."""
24
+ print(f"[RAG_SERVICE] Retrieving context for tenant '{self.tenant_id}' query: '{query[:80]}'")
25
+
26
+ raw = knowledge_service.search(query)
27
+
28
+ chunks: List[Dict[str, Any]] = []
29
+ if raw:
30
+ parts = [p for p in raw.split("\n\n--- ") if p.strip()]
31
+ for i, part in enumerate(parts[:top_k]):
32
+ chunks.append({
33
+ "id": f"{self.tenant_id}_chunk_{i}_{hashlib.md5(part.encode('utf-8')).hexdigest()[:8]}",
34
+ "content": part.strip(),
35
+ "score": max(0.0, 0.95 - (i * 0.03)),
36
+ "source": "knowledge_base",
37
+ })
38
+ return chunks
39
+
40
+ def add_knowledge_entry(self, question: str, answer: str) -> bool:
41
+ """Persists a verified Q&A into unknown_questions.json and re-ingests."""
42
+ try:
43
+ result = knowledge_service.save_verified_answer(question, answer)
44
+ print(f"[RAG_SERVICE] Active-learning entry for tenant '{self.tenant_id}': {result}")
45
+ return True
46
+ except Exception as e:
47
+ print(f"[RAG_SERVICE ERROR] Failed to persist Q&A for tenant '{self.tenant_id}': {e}")
48
+ return False
backend/app/services/session_store.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Thread-safe in-memory session state for the ArunCore backend.
2
+
3
+ Consolidates all live websocket-like stores that were previously scattered
4
+ through the API module:
5
+
6
+ * message history per session (visitor + twin + real-human entries)
7
+ * human-owned messages
8
+ * human-control flags (3-way live chat takeover)
9
+ * rolling memory objects per session
10
+
11
+ All reads/writes are guarded by a global lock so concurrent chat streams
12
+ cannot corrupt shared dicts.
13
+ """
14
+ import os
15
+ import time
16
+ import datetime
17
+ import threading
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from langchain_openai import ChatOpenAI
21
+
22
+ from backend.app.services.memory_manager import RollingMemory
23
+
24
+
25
+ def _default_summary_llm():
26
+ return ChatOpenAI(
27
+ temperature=0.0,
28
+ model=os.getenv("MEMORY_SUMMARY_MODEL", "gpt-4.1-nano"),
29
+ api_key=os.getenv("OPENAI_API_KEY"),
30
+ )
31
+
32
+
33
+ class SessionStore:
34
+ """Single ownership point for every in-memory session artifact."""
35
+
36
+ def __init__(self, summary_llm_factory: Any = None):
37
+ self._lock = threading.RLock()
38
+ self._history: Dict[str, List[Dict[str, Any]]] = {}
39
+ self._human_messages: Dict[str, List[Dict[str, Any]]] = {}
40
+ self._human_control: Dict[str, bool] = {}
41
+ self._memories: Dict[str, RollingMemory] = {}
42
+ self._summary_llm_factory = summary_llm_factory or _default_summary_llm
43
+
44
+ @property
45
+ def liveness(self) -> int:
46
+ return len(self._memories)
47
+
48
+ # ------------------------------------------------------------------ #
49
+ # Message history
50
+ # ------------------------------------------------------------------ #
51
+ def record_message(self, session_id, sender, text, name="", thoughts=None):
52
+ with self._lock:
53
+ now_str = datetime.datetime.now().strftime("%I:%M %p")
54
+ entry = {
55
+ "id": f"msg_{sender}_{int(time.time() * 1000)}_{len(self._history.get(session_id, []))}",
56
+ "sender": sender,
57
+ "name": name or (
58
+ "Arun Yadav" if sender == "human_arun"
59
+ else "Arun's AI Assistant" if sender == "twin"
60
+ else "You"
61
+ ),
62
+ "text": text,
63
+ "timestamp": now_str,
64
+ }
65
+ if thoughts:
66
+ entry["thoughts"] = thoughts
67
+
68
+ if session_id not in self._history:
69
+ self._history[session_id] = []
70
+ self._history[session_id].append(entry)
71
+ return entry
72
+
73
+ def get_history(self, session_id: str) -> List[Dict[str, Any]]:
74
+ with self._lock:
75
+ return list(self._history.get(session_id, []))
76
+
77
+ def get_human_messages(self, session_id: str) -> List[Dict[str, Any]]:
78
+ with self._lock:
79
+ return list(self._human_messages.get(session_id, []))
80
+
81
+ def append_human_message(self, session_id: str, entry: Dict[str, Any]) -> None:
82
+ with self._lock:
83
+ self._human_messages.setdefault(session_id, []).append(entry)
84
+
85
+ def get_human_entries(self, session_id: str) -> List[Dict[str, Any]]:
86
+ """Channel messages authored by the real human within this session."""
87
+ with self._lock:
88
+ return [m for m in self._history.get(session_id, []) if m.get("sender") == "human_arun"]
89
+
90
+ def get_last_user_message(self, session_id: str) -> str:
91
+ with self._lock:
92
+ for m in reversed(self._history.get(session_id, [])):
93
+ if m.get("sender") == "user":
94
+ return m.get("text", "")
95
+ return ""
96
+
97
+ # ------------------------------------------------------------------ #
98
+ # Human-control (3-way live takeover)
99
+ # ------------------------------------------------------------------ #
100
+ def is_human_control(self, session_id: str) -> bool:
101
+ with self._lock:
102
+ return bool(self._human_control.get(session_id, False))
103
+
104
+ def set_human_control(self, session_id: str, enabled: bool) -> None:
105
+ with self._lock:
106
+ self._human_control[session_id] = enabled
107
+
108
+ # ------------------------------------------------------------------ #
109
+ # Rolling memory per session
110
+ # ------------------------------------------------------------------ #
111
+ def get_or_create_memory(self, session_id: str) -> RollingMemory:
112
+ with self._lock:
113
+ if session_id not in self._memories:
114
+ self._memories[session_id] = RollingMemory(
115
+ summary_llm=self._summary_llm_factory(),
116
+ )
117
+ return self._memories[session_id]
118
+
119
+
120
+ # Backwards-compatible global store (used by the FastAPI layer).
121
+ session_store = SessionStore()
backend/app/services/tenant_service.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from typing import Optional, Dict, Any
4
+ from backend.app.schemas.tenant import (
5
+ BrandConfig, AgentConfig, ChatConfig, VoiceConfig, SEOConfig, SocialConfig, TenantFullConfig
6
+ )
7
+
8
+
9
+ class TenantService:
10
+ def __init__(self, base_dir: Optional[str] = None):
11
+ if base_dir:
12
+ self.base_dir = base_dir
13
+ else:
14
+ self.base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
15
+ self.tenants_dir = os.path.join(self.base_dir, "tenants")
16
+
17
+ def get_tenant_path(self, tutor_id: str) -> str:
18
+ clean_id = (tutor_id or "arun").strip().lower()
19
+ path = os.path.join(self.tenants_dir, clean_id)
20
+ if os.path.exists(path):
21
+ return path
22
+ return os.path.join(self.tenants_dir, "tenant_starter")
23
+
24
+ def load_tenant_config(self, tutor_id: Optional[str] = None) -> TenantFullConfig:
25
+ clean_id = (tutor_id or "arun").strip().lower()
26
+ if clean_id in ("arun", "default", "none"):
27
+ clean_id = "tenant_starter"
28
+
29
+ tenant_path = self.get_tenant_path(clean_id)
30
+ resolved_id = os.path.basename(tenant_path)
31
+ config_dir = os.path.join(tenant_path, "config")
32
+
33
+ # Load split JSON configs
34
+ brand_data = self._read_json(os.path.join(config_dir, "brand.json"), {"tutor_id": resolved_id, "name": "Arun Yadav", "title": "Arun's AI Assistant", "role": "AI Systems Architect β€’ Healthcare & Education", "avatar_url": "/profile_photo.png", "logo_url": "/logo.jpg", "primary_color": "#6366f1", "accent_color": "#818cf8", "theme": "dark", "cta_text": "Consult Arun"})
35
+ agent_data = self._read_json(os.path.join(config_dir, "agent.json"), {"tutor_id": resolved_id, "system_prompt": "You are Arun's AI Assistant.", "temperature": 0.3, "model": "gpt-4o", "enabled_tools": ["search_courses", "book_calendar", "faq_lookup"], "guardrails": []})
36
+ chat_data = self._read_json(os.path.join(config_dir, "chat.json"), {"tutor_id": resolved_id, "welcome_message": "Hi! I'm Arun's AI Assistant.", "suggested_questions": ["What projects has Arun built?", "How can I contact Arun?"]})
37
+ voice_data = self._read_json(os.path.join(config_dir, "voice.json"), {"tutor_id": resolved_id, "voice_id": "alloy", "model": "tts-1", "speed": 1.0})
38
+ seo_data = self._read_json(os.path.join(config_dir, "seo.json"), {"tutor_id": resolved_id, "meta_title": "Arun Core", "meta_description": "AI Platform", "keywords": []})
39
+ social_data = self._read_json(os.path.join(config_dir, "social.json"), {"tutor_id": resolved_id, "website": "https://neuralarun.in"})
40
+
41
+ brand_data["tutor_id"] = resolved_id
42
+ agent_data["tutor_id"] = resolved_id
43
+ chat_data["tutor_id"] = resolved_id
44
+ voice_data["tutor_id"] = resolved_id
45
+ seo_data["tutor_id"] = resolved_id
46
+ social_data["tutor_id"] = resolved_id
47
+
48
+ return TenantFullConfig(
49
+ tutor_id=resolved_id,
50
+ brand=BrandConfig(**brand_data),
51
+ agent=AgentConfig(**agent_data),
52
+ chat=ChatConfig(**chat_data),
53
+ voice=VoiceConfig(**voice_data),
54
+ seo=SEOConfig(**seo_data),
55
+ social=SocialConfig(**social_data),
56
+ )
57
+
58
+ def _read_json(self, filepath: str, fallback: Dict[str, Any]) -> Dict[str, Any]:
59
+ if os.path.exists(filepath):
60
+ try:
61
+ with open(filepath, "r", encoding="utf-8") as f:
62
+ return json.load(f)
63
+ except Exception as e:
64
+ print(f"[TENANT_SERVICE WARNING] Failed to parse {filepath}: {e}")
65
+ return fallback
66
+
67
+ # ------------------------------------------------------------------ #
68
+ # Legacy demos-dictionary loader (pre-dates split configs). #
69
+ # Kept here so both config systems resolve in one service and the #
70
+ # agent loop reuses it for `?tutor=<id>` responses. #
71
+ # ------------------------------------------------------------------ #
72
+ def load_legacy_tutor_config(self, tutor_id: Optional[str]) -> Optional[Dict[str, Any]]:
73
+ slug = (tutor_id or "arun").strip().lower()
74
+
75
+ candidate_paths = [
76
+ os.path.join(self.base_dir, "demos", f"{slug}_enterprise_dictionary.json"),
77
+ os.path.join(self.base_dir, "demos", f"{slug}.json"),
78
+ os.path.join(self.base_dir, "data", "leads", f"{slug}_enterprise_dictionary.json"),
79
+ os.path.join(self.base_dir, "data", "leads", f"{slug}.json"),
80
+ os.path.join(self.base_dir, "demos", "general.json"),
81
+ ]
82
+
83
+ for path in candidate_paths:
84
+ if os.path.exists(path):
85
+ try:
86
+ with open(path, "r", encoding="utf-8") as f:
87
+ data = json.load(f)
88
+ if data.get("client_id") == "{{TUTOR_ID}}":
89
+ data["client_id"] = slug
90
+ data["tutor_id"] = slug
91
+ return data
92
+ except Exception as e:
93
+ print(f"[LOAD TUTOR CONFIG ERROR] Failed reading {path}: {e}")
94
+
95
+ return None
96
+
97
+
98
+ # Singleton instance helper
99
+ tenant_service = TenantService()
backend/app/services/tool_executor.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Config-driven tool registry for the ArunCore agent.
2
+
3
+ Every tool the LLM may call is defined here and exposed through `ToolExecutor`.
4
+ A tenant's `enabled_tools` array (agent.json) simply selects which tools get
5
+ bound into the LLM execution loop β€” no Python edits required to add a client.
6
+ """
7
+ from typing import Dict, Any, List, Optional
8
+
9
+ from langchain_core.tools import tool
10
+
11
+ from backend.app.services.knowledge_service import knowledge_service
12
+ from backend.app.services.notification_service import schedule_notify_arun
13
+
14
+
15
+ # ---------------------------------------------------------------------- #
16
+ # Legacy placeholder tools (kept for tenant agent.json enabled_tools and #
17
+ # the demo dictionary path; not bound by the core Arun twin agent). #
18
+ # ---------------------------------------------------------------------- #
19
+ @tool
20
+ def search_courses(query: str) -> str:
21
+ """Search tenant course catalog and curriculum."""
22
+ return f"Retrieved course details for query: '{query}'."
23
+
24
+
25
+ @tool
26
+ def book_calendar(date: str, topic: str) -> str:
27
+ """Book a consultation session or mentorship call."""
28
+ return f"Consultation session requested for '{topic}' on {date}."
29
+
30
+
31
+ @tool
32
+ def faq_lookup(question: str) -> str:
33
+ """Search tenant FAQ knowledge base."""
34
+ return f"Retrieved verified FAQ answers for: '{question}'."
35
+
36
+
37
+ # ---------------------------------------------------------------------- #
38
+ # ArunCore real agent tools #
39
+ # ---------------------------------------------------------------------- #
40
+ @tool
41
+ def search_arun_knowledge(query: str) -> str:
42
+ """Search Arun's local knowledge base for information about his projects, architecture, philosophy, and background."""
43
+ return knowledge_service.search(query)
44
+
45
+
46
+ @tool
47
+ def get_github_live_data(username: str = "neural-arun") -> str:
48
+ """Fetch live GitHub repository data and recent commits for Arun Yadav."""
49
+ return knowledge_service.fetch_live_github(username)
50
+
51
+
52
+ @tool
53
+ def notify_arun(category: str, user_input: str, user_metadata_json: str = "") -> str:
54
+ """Send an instant Telegram alert to Arun's phone ONLY when a visitor explicitly asks to hire, consult, or contact Arun (LEAD), asks an unknown technical question (UNKNOWN_QUESTION), or requests urgent assistance (URGENT). Do NOT call this tool for general questions or identity questions like 'who are you'."""
55
+ schedule_notify_arun(category, user_input, user_metadata_json)
56
+ return (
57
+ f"Successfully sent Telegram alert to Arun's phone (Category: {category.upper()}).\n"
58
+ "YOU MUST NOW OUTPUT ARUN'S DIRECT CONTACT DETAILS IN BULLET POINTS:\n"
59
+ "- πŸ“ž Phone: +91 8881109193\n"
60
+ "- πŸ’¬ WhatsApp: https://wa.me/918881109193\n"
61
+ "- βœ‰οΈ Email: neural.arun.dev@gmail.com\n"
62
+ "- πŸ’Ό LinkedIn: https://www.linkedin.com/in/arun-yadav-768052368\n"
63
+ "- 🌐 GitHub: https://github.com/neural-arun"
64
+ )
65
+
66
+
67
+ class ToolExecutor:
68
+ """Dynamic, config-driven tool registry bound into the LLM loop."""
69
+
70
+ DEFAULT_TOOLS = ["search_arun_knowledge", "get_github_live_data", "notify_arun"]
71
+
72
+ AVAILABLE_TOOLS = {
73
+ "search_courses": search_courses,
74
+ "book_calendar": book_calendar,
75
+ "faq_lookup": faq_lookup,
76
+ "search_arun_knowledge": search_arun_knowledge,
77
+ "get_github_live_data": get_github_live_data,
78
+ "notify_arun": notify_arun,
79
+ }
80
+
81
+ @classmethod
82
+ def get_enabled_tools(cls, enabled_tool_names: Optional[List[str]]) -> List[Any]:
83
+ names = enabled_tool_names or cls.DEFAULT_TOOLS
84
+ tools: List[Any] = []
85
+ for name in names:
86
+ if name in cls.AVAILABLE_TOOLS:
87
+ tools.append(cls.AVAILABLE_TOOLS[name])
88
+ return tools
89
+
90
+ @classmethod
91
+ def get_tool_map(cls, enabled_tool_names: Optional[List[str]]) -> Dict[str, Any]:
92
+ return {t.name: t for t in cls.get_enabled_tools(enabled_tool_names)}
backend/app/services/voice_service.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import requests
4
+ from typing import Optional
5
+
6
+
7
+ class VoiceService:
8
+ @staticmethod
9
+ def generate_tts_audio(text: str, voice: str = "alloy") -> bytes:
10
+ openai_key = os.getenv("OPENAI_API_KEY")
11
+ if not openai_key:
12
+ raise ValueError("OPENAI_API_KEY environment variable missing.")
13
+
14
+ clean_text = (
15
+ text.replace("*", "")
16
+ .replace("#", "")
17
+ .replace("`", "")
18
+ .replace("\n", " ")
19
+ [:1000]
20
+ )
21
+
22
+ res = requests.post(
23
+ "https://api.openai.com/v1/audio/speech",
24
+ headers={
25
+ "Authorization": f"Bearer {openai_key}",
26
+ "Content-Type": "application/json",
27
+ },
28
+ json={
29
+ "model": "tts-1",
30
+ "input": clean_text,
31
+ "voice": voice or "alloy",
32
+ },
33
+ timeout=15,
34
+ )
35
+
36
+ if res.status_code == 200:
37
+ return res.content
38
+ else:
39
+ raise RuntimeError(f"OpenAI TTS API error {res.status_code}: {res.text}")
data/HOW_TO_UPDATE_DATA.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # How to Update ArunCore Knowledge Data
2
+
3
+ ArunCore uses a modular single-source-of-truth structure for your project data.
4
+
5
+ ---
6
+
7
+ ## πŸ“ Data Structure Overview
8
+
9
+ ```
10
+ data/
11
+ β”œβ”€β”€ static/
12
+ β”‚ β”œβ”€β”€ public_profile.md # Your core identity, principles, vision & loop
13
+ β”‚ └── rules_of_engagement.md # LLM guidelines & zero-hallucination rules
14
+ β”‚
15
+ └── github/ # ONE clean folder per GitHub project
16
+ β”œβ”€β”€ <project_name>/
17
+ β”‚ β”œβ”€β”€ README.md # Project overview, tech stack, & direct GitHub URL link
18
+ β”‚ └── metadata.json # Structured metadata (URL, stack, stars, topics)
19
+ └── ...
20
+ ```
21
+
22
+ ---
23
+
24
+ ## πŸš€ How to Add or Update a Project
25
+
26
+ ### Option A: Automatic GitHub Sync (Recommended)
27
+ Run the automated sync script to pull your latest READMEs and GitHub URLs directly from your GitHub profile (`neural-arun`):
28
+
29
+ ```bash
30
+ python scripts/sync_github_data.py
31
+ ```
32
+
33
+ ### Option B: Manual Add
34
+ 1. Create a new folder under `data/github/<your_repo_name>/`.
35
+ 2. Place your project `README.md` and `metadata.json` inside it.
36
+ 3. Make sure `README.md` starts with a link header:
37
+ ```markdown
38
+ > **GitHub Repository:** [https://github.com/neural-arun/your_repo_name](https://github.com/neural-arun/your_repo_name)
39
+ ```
40
+
41
+ ---
42
+
43
+ ## πŸ”„ Re-Ingest into Vector DB
44
+
45
+ After adding or modifying files in `data/`:
46
+
47
+ ```bash
48
+ python core/ingest.py
49
+ ```
50
+
51
+ `ingest.py` incrementally updates ChromaDB so your agent instantly gains access to the new knowledge.
data/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ“Š ArunCore Data Directory (`data/`)
2
+
3
+ This directory contains the knowledge base, static profile documents, raw backgrounds, and GitHub repository README files used for retrieval-augmented generation.
4
+
5
+ ---
6
+
7
+ ## πŸ“ Data Subdirectories & Files
8
+
9
+ ```text
10
+ data/
11
+ β”œβ”€β”€ github/ # 21 subdirectories containing raw README.md files for each repo
12
+ β”‚ β”œβ”€β”€ aruncore/ # ArunCore RAG Engine README
13
+ β”‚ β”œβ”€β”€ legal_RAG_system/ # Legal RAG System README
14
+ β”‚ β”œβ”€β”€ med_coach/ # MedCoach Clinical Reasoning README
15
+ β”‚ β”œβ”€β”€ neet-bot/ # NEET Medical Bot README
16
+ β”‚ β”œβ”€β”€ real_state_listing_scraper/ # 99acres Scraper README
17
+ β”‚ └── ... (21 total public repositories)
18
+ β”œβ”€β”€ linkedin/
19
+ β”‚ β”œβ”€β”€ posts.md # Scraped public LinkedIn posts & technical insights
20
+ β”‚ └── profile_summary.md # Public LinkedIn profile summary
21
+ β”œβ”€β”€ raw/
22
+ β”‚ β”œβ”€β”€ personal_background.md # Deeper origin story, NEET pivot, JEE journey, & working style
23
+ β”‚ └── unknown_questions.json # Verified Q&A store for active learning alerts
24
+ └── static/
25
+ β”œβ”€β”€ public_profile.md # Master public technical profile & architecture specs
26
+ β”œβ”€β”€ rules_of_engagement.md # Steering rules, search mandates, & guardrails
27
+ └── voice_persona.md # Custom sharp & witty voice persona definition
28
+ ```
29
+
30
+ ---
31
+
32
+ ## πŸ”„ Auto-Syncing Data
33
+
34
+ - **Sync GitHub READMEs**: `python3 scripts/sync_github.py`
35
+ - **Sync LinkedIn Posts**: `python3 scripts/sync_linkedin.py`
36
+ - **1-Click Master Sync**: `python3 scripts/sync_all.py`
37
+ - **Vector Ingestion**: `python3 scripts/ingest.py`
data/github/01_manage_patient_task/README.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ project_name: 01_manage_patient_task
3
+ github_url: https://github.com/neural-arun/01_manage_patient_task
4
+ language: Python
5
+ stars: 0
6
+ topics: [None]
7
+ updated_at: 2026-06-28T11:34:30Z
8
+ ---
9
+
10
+ # 01_manage_patient_task
11
+
12
+ > **GitHub Repository:** [https://github.com/neural-arun/01_manage_patient_task](https://github.com/neural-arun/01_manage_patient_task)
13
+ > **Primary Language:** Python | **Stars:** 0 | **Forks:** 0
14
+ > **Description:** No description provided.
15
+
16
+ ---
17
+
18
+ # Patient Task Management API
19
+
20
+ **Build 1** of the FastAPI for Healthcare + Medical Education Systems roadmap.
21
+
22
+ A CRUD API for managing patient tasks β€” medication reminders, daily activities, and completion tracking β€” built with FastAPI.
23
+
24
+ ## Features
25
+
26
+ - Create a patient task
27
+ - List all tasks (with optional status filtering)
28
+ - Get a single task by ID
29
+ - Update a task
30
+ - Mark a task as complete
31
+ - Delete a task
32
+
33
+ ## Tech Stack
34
+
35
+ - **FastAPI** β€” web framework
36
+ - **Uvicorn** β€” ASGI server
37
+ - **Pydantic** β€” request/response validation
38
+ - **Python 3.10+**
39
+
40
+ ## Project Structure
41
+
42
+ ```
43
+ 01_manage_patient_task/
44
+ β”œβ”€β”€ main.py # App entry point & route definitions
45
+ β”œβ”€β”€ models.py # Pydantic schemas
46
+ β”œβ”€β”€ notes/ # Study notes (learning reference)
47
+ β”œβ”€β”€ requirements.txt # Dependencies
48
+ └── README.md
49
+ ```
50
+
51
+ ## Setup
52
+
53
+ ```bash
54
+ python -m venv .venv
55
+ source .venv/bin/activate
56
+ pip install -r requirements.txt
57
+ ```
58
+
59
+ ## Run
60
+
61
+ ```bash
62
+ uvicorn main:app --reload
63
+ ```
64
+
65
+ - API: `http://127.0.0.1:8000`
66
+ - Docs: `http://127.0.0.1:8000/docs`
67
+ - ReDoc: `http://127.0.0.1:8000/redoc`
68
+
69
+ ## API Endpoints
70
+
71
+ | Method | Endpoint | Description |
72
+ |----------|--------------------------|--------------------|
73
+ | `POST` | `/tasks` | Create a task |
74
+ | `GET` | `/tasks` | List tasks |
75
+ | `GET` | `/tasks/{task_id}` | Get a task |
76
+ | `PUT` | `/tasks/{task_id}` | Update a task |
77
+ | `PATCH` | `/tasks/{task_id}/complete` | Mark complete |
78
+ | `DELETE` | `/tasks/{task_id}` | Delete a task |
79
+
80
+ ## Topics Covered
81
+
82
+ - FastAPI fundamentals & ASGI
83
+ - Routing (GET, POST, PUT, PATCH, DELETE)
84
+ - Path & query parameters
85
+ - Request body & Pydantic validation
86
+ - Response models & status codes
87
+ - Error handling with HTTPException
data/github/01_manage_patient_task/metadata.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "01_manage_patient_task",
3
+ "github_url": "https://github.com/neural-arun/01_manage_patient_task",
4
+ "description": "No description provided.",
5
+ "language": "Python",
6
+ "stars": 0,
7
+ "forks": 0,
8
+ "topics": [],
9
+ "updated_at": "2026-06-28T11:34:30Z"
10
+ }
data/github/Agentic_AI_Projects/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ project_name: Agentic_AI_Projects
3
+ github_url: https://github.com/neural-arun/Agentic_AI_Projects
4
+ language: Python
5
+ stars: 0
6
+ topics: [None]
7
+ updated_at: 2026-03-22T07:20:56Z
8
+ ---
9
+
10
+ # Agentic_AI_Projects
11
+
12
+ > **GitHub Repository:** [https://github.com/neural-arun/Agentic_AI_Projects](https://github.com/neural-arun/Agentic_AI_Projects)
13
+ > **Primary Language:** Python | **Stars:** 0 | **Forks:** 0
14
+ > **Description:** No description provided.
15
+
16
+ ---
17
+
18
+ *No README.md file found in this repository on GitHub.*
19
+
20
+ ## Overview
21
+ No description provided.
data/github/Agentic_AI_Projects/metadata.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Agentic_AI_Projects",
3
+ "github_url": "https://github.com/neural-arun/Agentic_AI_Projects",
4
+ "description": "No description provided.",
5
+ "language": "Python",
6
+ "stars": 0,
7
+ "forks": 0,
8
+ "topics": [],
9
+ "updated_at": "2026-03-22T07:20:56Z"
10
+ }
data/github/ArunCore/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ArunCore β€” Production Personal AI Assistant & Portfolio
2
+
3
+ > **GitHub Repository:** [https://github.com/neural-arun/ArunCore](https://github.com/neural-arun/ArunCore)
4
+ > **Live Web Application:** [https://aruncore.vercel.app](https://aruncore.vercel.app)
5
+
6
+ ArunCore is an agentic, stateful personal AI twin built for **Arun Yadav** (AI Systems Architect specializing in Healthcare & Education).
7
+
8
+ ---
9
+
10
+ ## Key Features
11
+ - **100% Automated Telegram Alerts**: Every visitor interaction automatically sends a notification to Arun's phone.
12
+ - **1-Click Magic Link 3-Way Real Human Takeover**: Enables Arun to join ongoing web chat sessions live from Telegram via a 1-click magic link.
13
+ - **Vercel Serverless Egress Relay**: Routes Telegram API traffic through Vercel serverless routes (`/api/telegram`) to eliminate firewall timeouts.
14
+ - **Active Learning Vector Store**: Stores verified human answers from Telegram replies into `data/raw/unknown_questions.json` and re-indexes ChromaDB.
15
+ - **Zero-Hallucination RAG**: Dense vector retrieval + BM25 keyword search + Cohere V3 Reranker.
16
+ - **Dynamic Language Rules**: Responds in 100% articulate English for English queries, matching user tone cleanly.