AI Agent commited on
Commit ·
d2aec69
1
Parent(s): 56cb286
Remove excess documentation and CLI scripts to keep HF repo lean
Browse files- NITDAA_ARCHITECTURE_DESIGN.md +0 -32
- NITDAA_ARCHITECTURE_DESIGN.pdf +0 -3
- NITDAA_HEALTHEXPERT_USER_GUIDE.md +0 -36
- NITDAA_HEALTHEXPERT_USER_GUIDE.pdf +0 -3
- NITDAA_HF_DEPLOYMENT_GUIDE.md +0 -36
- docker-compose.yml +0 -29
- healthexpert.py +0 -111
- manage_db.py +0 -93
- manage_llm.py +0 -159
- pytest.ini +0 -17
NITDAA_ARCHITECTURE_DESIGN.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
# Nitdaa Sibling Project - Architecture Design
|
| 2 |
-
**Version 1.0 Public Release**
|
| 3 |
-
|
| 4 |
-
## 1. System Overview
|
| 5 |
-
Nitdaa is the mobile-friendly MVP sibling to the full HealthExpert enterprise platform. It removes administrative overhead, heavy RBAC UI components, and complex multi-pane layouts in favor of a lean, mobile-first edge interface.
|
| 6 |
-
|
| 7 |
-
## 2. Core Differences from Base HealthExpert
|
| 8 |
-
- **UI/UX**: Transitioned from a 3-pane desktop layout to a single-pane vertical mobile layout, featuring dynamic inline controls (e.g. the bottom input panel temporarily transforms into a 5-star & thumbs feedback interface after response generation).
|
| 9 |
-
- **Admin Constraints**: Admin controls (Kill Switch, DB Purge) are entirely hidden in the HTML to prevent public tampering.
|
| 10 |
-
- **Session Audit**: Uses `nitdaa_sessions.json` and `nitdaa_summary.json` for granular system telemetry rather than database-backed logs. Captures granular feedback (1-5 stars, thumbs, and text) instantly upon user action.
|
| 11 |
-
- **Resumable Event Streaming**: The architecture uses an asynchronous Job ID system (`/api/query/start` and `/api/query/stream`) to survive mobile background disconnections.
|
| 12 |
-
- **Concurrent RAG Isolation**: Vector and Graph retrieval pipelines operate inside a Thread Pool with thread-local database connections. Heavy PyTorch inference layers (CrossEncoder and SentenceTransformer) are strictly serialized via threading locks to prevent `OutOfMemory` crashes on resource-constrained infrastructure, while keeping I/O concurrency high.
|
| 13 |
-
|
| 14 |
-
## 3. Data Flow
|
| 15 |
-
1. **Startup & Sync**: Flask initializes. `sync_he_data()` runs asynchronously to pull the latest dataset from `Sam-max1/he-data`. If differences are found, it gracefully purges vector/graph databases and rebuilds. Concurrently, it performs a 2-way remote synchronization of logs with `Sam-max1/mat_data`.
|
| 16 |
-
2. **Security & Connection**: Client connections face an immediate Math CAPTCHA. All API calls pass through `Flask-Limiter` and Strict HTTP Content Security Policies.
|
| 17 |
-
3. **Ingestion**: If new files are detected, `process_document_pipeline` splits them into 512-token chunks, vectorizes them into ChromaDB, and extracts graph entities locally using `spaCy`.
|
| 18 |
-
4. **Retrieval (Concurrent)**: When a query hits `/api/query/start`, an asynchronous thread pool handles Tri-modal Hybrid Retrieval (Vector + BM25 + Graph) allowing multiple users to fetch context simultaneously.
|
| 19 |
-
5. **Synthesis & Feedback**: A Cross-Encoder reranks the chunks, and the context is fed into the local LLM generation queue. The frontend streams events using Server-Sent Events with offset recovery. Upon completion, users optionally submit ratings securely stored in `nitdaa_summary.json`.
|
| 20 |
-
|
| 21 |
-
## 4. Resource Allocation
|
| 22 |
-
Nitdaa is tuned for the HuggingFace Spaces free tier:
|
| 23 |
-
- **vCPU constraints**: Threads are explicitly clamped to prevent CPU thrashing.
|
| 24 |
-
- **Vector Limit**: A hard limit of 10,000 chunks is enforced globally to prevent `OutOfMemory` crashes.
|
| 25 |
-
- **Session Quotas**: Max 5 file uploads per session to prevent abuse.
|
| 26 |
-
|
| 27 |
-
## 5. Project Intelligence & Deployment
|
| 28 |
-
- **HuggingFace Space**: [Sam-max1/nitdaa](https://huggingface.co/spaces/Sam-max1/nitdaa)
|
| 29 |
-
- **HuggingFace ID**: `sam-max1`
|
| 30 |
-
- **Authentication**: `HF_TOKEN` is provided via the system keyring for secure access to HuggingFace resources and pushing updates.
|
| 31 |
-
- **Deployment Protocol**: Code base updates are pushed directly to the HuggingFace Space repository. Note: Core functionality should not be altered, deleted, or impaired unless explicitly requested.
|
| 32 |
-
- **Minimalist Deployment**: Nitdaa must always contain only the bare minimum files required to run the application. Extraneous resources, local databases, or unneeded assets must be excluded from tracking to ensure only the bare minimum is pushed to HuggingFace.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NITDAA_ARCHITECTURE_DESIGN.pdf
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:d06a069f37aa6efe19cc97d445336a38689930c94aabb1e059948b90238ed5c4
|
| 3 |
-
size 72541
|
|
|
|
|
|
|
|
|
|
|
|
NITDAA_HEALTHEXPERT_USER_GUIDE.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
# Nitdaa HealthExpert Mobile MVP - User Guide
|
| 2 |
-
**Version 1.0 Public Release**
|
| 3 |
-
|
| 4 |
-
## Overview
|
| 5 |
-
Nitdaa HealthExpert is a mobile-optimized, single-pane web interface designed to provide users with direct access to the HealthExpert AI knowledge base. Deployed on HuggingFace Spaces, Nitdaa brings the power of Agentic AI, Hybrid RAG, and Edge Compute into a sandboxed, responsive environment.
|
| 6 |
-
|
| 7 |
-
## 1. Accessing the Application
|
| 8 |
-
- Navigate to the official public application link: [https://sam-max1-nitdaa.hf.space/](https://sam-max1-nitdaa.hf.space/)
|
| 9 |
-
- **Security Check (CAPTCHA)**: Upon your first visit, a security modal will challenge you with a simple math problem (e.g. `5 + 3 = ?`). You must correctly solve this problem to unlock the application dashboard.
|
| 10 |
-
- The application will then load automatically. Note that the first boot may take 3-5 minutes as the system pulls the latest knowledge datasets securely from the cloud.
|
| 11 |
-
- Once the **Status Pill** in the top-right corner turns **Green** (Ready), the system is online.
|
| 12 |
-
|
| 13 |
-
## 2. Using the Dashboard
|
| 14 |
-
The dashboard is designed for mobile efficiency.
|
| 15 |
-
- **Top Header**: Displays the application branding ("Healthcare Policy Expert") and current status.
|
| 16 |
-
- **Scrollable Output Window**: The large central area displays the conversation history. It automatically scrolls down as new answers are generated.
|
| 17 |
-
- **Input Area**: Located at the bottom of the screen.
|
| 18 |
-
|
| 19 |
-
## 3. Asking Questions
|
| 20 |
-
You can query the system in two ways:
|
| 21 |
-
1. **Quick Questions Dropdown**: Tap the dropdown menu above the text input to select a preset question. Selecting a question will automatically submit it to the AI.
|
| 22 |
-
2. **Manual Input**: Type your query directly into the text box and press the **Send** button.
|
| 23 |
-
|
| 24 |
-
## 4. Understanding Output
|
| 25 |
-
When a question is submitted:
|
| 26 |
-
- Your question will instantly appear in the output window.
|
| 27 |
-
- The system will display a **"Generating answer.. (1.2s)"** timer.
|
| 28 |
-
- Once context is retrieved from the Vector and Graph databases, the LLM will stream the Markdown-formatted answer back to the screen.
|
| 29 |
-
- Analysis may take upto 300 seconds due to slow shared resources. Please keep patience.
|
| 30 |
-
- **Providing Feedback**: Once an answer completes, the bottom input area will temporarily switch to a Feedback panel. You will see Thumbs Up (👍) / Thumbs Down (👎) buttons alongside a 5-star rating widget (where colors transition from red to green). Selecting an icon captures your feedback immediately. You may optionally enter text feedback and click **Submit**, or click **Cancel** to revert the panel back to the standard question box.
|
| 31 |
-
- Responses remain preserved in the scrollable window for the duration of your session.
|
| 32 |
-
|
| 33 |
-
## 5. Security & Session Handling
|
| 34 |
-
- **Multi-tenant Isolation**: Your conversation is strictly isolated from other users using an invisible `X-Session-Token`. Multiple users can query the system concurrently without interference.
|
| 35 |
-
- **Resumable Mobile Sessions**: If your mobile device goes to sleep or you navigate to another app, the system safely buffers your answer. When you return, the streaming will seamlessly resume without missing a word.
|
| 36 |
-
- **Session Expiration**: If the page is reloaded, or if you are inactive for an extended period, your session token resets, and previous context is cleared to ensure privacy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NITDAA_HEALTHEXPERT_USER_GUIDE.pdf
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:0107e7f398e33e944638cc391375987e0eaa82cfd50f2e15525c94be98fad98e
|
| 3 |
-
size 65952
|
|
|
|
|
|
|
|
|
|
|
|
NITDAA_HF_DEPLOYMENT_GUIDE.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
# Nitdaa Sibling Project - HF Deployment Guide
|
| 2 |
-
**Version 1.0 Public Release**
|
| 3 |
-
|
| 4 |
-
## Overview
|
| 5 |
-
This document outlines the specific steps and constraints required to deploy the Nitdaa mobile sibling to HuggingFace Spaces free tier.
|
| 6 |
-
|
| 7 |
-
## 1. Prerequisites
|
| 8 |
-
- A HuggingFace account.
|
| 9 |
-
- A HuggingFace Space configured with the **Docker** SDK.
|
| 10 |
-
- Hardware allocation: Free Tier (2 vCPU, 16GB Disk, 12GB RAM).
|
| 11 |
-
|
| 12 |
-
## 2. HuggingFace Secrets Configuration
|
| 13 |
-
To enable dynamic Dataset Synchronization, you must configure the following variable:
|
| 14 |
-
- `HF_TOKEN`: A HuggingFace access token with "Read" and "Write" permissions to the `Sam-max1/he-data` and `Sam-max1/mat_data` datasets. This token is available via the system keyring or environment variables.
|
| 15 |
-
- `ADMIN_MODE`: Set to `0` to completely lock down the UI for public usage.
|
| 16 |
-
|
| 17 |
-
## 2.1 Minimalist Deployment Rule
|
| 18 |
-
Nitdaa must always contain **only the bare minimum files required** to run the application in HF and the bare minimum files to be pushed to HF. Exclude all extraneous assets, development scripts, or unnecessary documentation from the Hugging Face repository.
|
| 19 |
-
|
| 20 |
-
## 3. Deployment Steps
|
| 21 |
-
1. Clone the `nitdaa` folder locally.
|
| 22 |
-
2. Initialize a git repository pointing to your HF Space remote.
|
| 23 |
-
3. Use the HuggingFace CLI to upload large directories without timing out:
|
| 24 |
-
```bash
|
| 25 |
-
huggingface-cli upload <your_username>/nitdaa . . --repo-type space
|
| 26 |
-
```
|
| 27 |
-
4. HuggingFace will automatically detect the `Dockerfile` in the root and begin building.
|
| 28 |
-
|
| 29 |
-
## 4. Cold Boot Times
|
| 30 |
-
Due to the CPU-bound nature of the free tier:
|
| 31 |
-
- **Docker Build**: ~5-8 minutes (first boot only).
|
| 32 |
-
- **Dataset Sync**: Variable depending on the size of `he_data`.
|
| 33 |
-
- **Model Load**: The `Qwen3.5-2B` GGUF model and PyTorch embeddings take ~1-2 minutes to spin up into memory on boot.
|
| 34 |
-
|
| 35 |
-
## 5. Security Safeguards
|
| 36 |
-
Nitdaa implements headless API blocking. Any direct programmatic access to the REST endpoints (without proper CORS headers or session tokens) will automatically return `403 Forbidden` to prevent bot-net abuse on the public URL.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docker-compose.yml
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
services:
|
| 2 |
-
neo4j:
|
| 3 |
-
image: neo4j:5.18-community
|
| 4 |
-
container_name: healthexpert-neo4j
|
| 5 |
-
ports:
|
| 6 |
-
- "7474:7474" # Neo4j Browser UI
|
| 7 |
-
- "7687:7687" # Bolt protocol
|
| 8 |
-
environment:
|
| 9 |
-
- NEO4J_AUTH=neo4j/healthexpert
|
| 10 |
-
- NEO4J_server_memory_heap_initial__size=512m
|
| 11 |
-
- NEO4J_server_memory_heap_max__size=1G
|
| 12 |
-
- NEO4J_PLUGINS=["apoc"]
|
| 13 |
-
- NEO4J_dbms_security_procedures_unrestricted=apoc.*
|
| 14 |
-
- NEO4J_dbms_security_procedures_allowlist=apoc.*
|
| 15 |
-
volumes:
|
| 16 |
-
- neo4j_data:/data
|
| 17 |
-
- neo4j_logs:/logs
|
| 18 |
-
- neo4j_import:/var/lib/neo4j/import
|
| 19 |
-
healthcheck:
|
| 20 |
-
test: ["CMD", "wget", "-q", "--spider", "http://localhost:7474"]
|
| 21 |
-
interval: 10s
|
| 22 |
-
timeout: 5s
|
| 23 |
-
retries: 10
|
| 24 |
-
restart: unless-stopped
|
| 25 |
-
|
| 26 |
-
volumes:
|
| 27 |
-
neo4j_data:
|
| 28 |
-
neo4j_logs:
|
| 29 |
-
neo4j_import:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
healthexpert.py
DELETED
|
@@ -1,111 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Document AI Expert — Standalone CLI
|
| 4 |
-
=========================================
|
| 5 |
-
Usage:
|
| 6 |
-
python healthexpert.py ingest <file_path>
|
| 7 |
-
python healthexpert.py query "<question>"
|
| 8 |
-
python healthexpert.py list
|
| 9 |
-
python healthexpert.py clear <source_name>
|
| 10 |
-
python healthexpert.py status
|
| 11 |
-
"""
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
import sys, os
|
| 14 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
-
|
| 16 |
-
import argparse
|
| 17 |
-
import config
|
| 18 |
-
from pipeline import vector_store, graph_store, embedder, document_loader, chunker
|
| 19 |
-
from agents.crew import run_ingest_crew, run_query_crew
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def cmd_ingest(file_path: str) -> None:
|
| 23 |
-
if not os.path.exists(file_path):
|
| 24 |
-
print(f"[ERROR] File not found: {file_path}")
|
| 25 |
-
sys.exit(1)
|
| 26 |
-
print(f"[INGEST] Processing: {file_path}")
|
| 27 |
-
result = run_ingest_crew(file_path)
|
| 28 |
-
print(f"\n[RESULT]\n{result}")
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def cmd_query(question: str) -> None:
|
| 32 |
-
if vector_store.count() == 0:
|
| 33 |
-
print("[ERROR] No documents ingested. Run: python healthexpert.py ingest <file>")
|
| 34 |
-
sys.exit(1)
|
| 35 |
-
print(f"[QUERY] {question}\n")
|
| 36 |
-
answer = run_query_crew(question)
|
| 37 |
-
print("\n" + "=" * 60)
|
| 38 |
-
print(answer)
|
| 39 |
-
print("=" * 60)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def cmd_list() -> None:
|
| 43 |
-
docs = vector_store.list_documents()
|
| 44 |
-
if not docs:
|
| 45 |
-
print("[INFO] No documents ingested yet.")
|
| 46 |
-
return
|
| 47 |
-
print(f"\n{'SOURCE':<40} {'TYPE':<10}")
|
| 48 |
-
print("-" * 52)
|
| 49 |
-
for d in docs:
|
| 50 |
-
print(f"{d['source']:<40} {d['file_type']:<10}")
|
| 51 |
-
print(f"\nTotal: {len(docs)} document(s), {vector_store.count()} chunk(s)")
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def cmd_clear(source_name: str) -> None:
|
| 55 |
-
n = vector_store.delete_document(source_name)
|
| 56 |
-
graph_store.delete_source(source_name)
|
| 57 |
-
print(f"[CLEAR] Deleted {n} chunks for '{source_name}'")
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def cmd_status() -> None:
|
| 61 |
-
print("\n── Document AI Expert Status ──")
|
| 62 |
-
print(f" LLM Endpoint : {config.LLM_BASE_URL}")
|
| 63 |
-
print(f" LLM Model : {config.LLM_MODEL_ID}")
|
| 64 |
-
print(f" Embedding : {config.EMBEDDING_MODEL} ({config.EMBEDDING_DEVICE})")
|
| 65 |
-
print(f" Vector DB : {vector_store.count()} chunks [{config.WEAVIATE_URL}]")
|
| 66 |
-
g = graph_store.get_stats()
|
| 67 |
-
if g.get("available"):
|
| 68 |
-
print(f" Graph DB : {g['nodes']} nodes, {g['relationships']} relationships")
|
| 69 |
-
else:
|
| 70 |
-
print(" Graph DB : OFFLINE (vector-only mode)")
|
| 71 |
-
print()
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def main():
|
| 75 |
-
parser = argparse.ArgumentParser(
|
| 76 |
-
description="Document AI Expert — CLI",
|
| 77 |
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 78 |
-
epilog=__doc__,
|
| 79 |
-
)
|
| 80 |
-
sub = parser.add_subparsers(dest="command")
|
| 81 |
-
|
| 82 |
-
p_ingest = sub.add_parser("ingest", help="Ingest a document")
|
| 83 |
-
p_ingest.add_argument("file", help="Path to document file")
|
| 84 |
-
|
| 85 |
-
p_query = sub.add_parser("query", help="Ask a question")
|
| 86 |
-
p_query.add_argument("question", help="Question string")
|
| 87 |
-
|
| 88 |
-
sub.add_parser("list", help="List ingested documents")
|
| 89 |
-
sub.add_parser("status", help="Show system status")
|
| 90 |
-
|
| 91 |
-
p_clear = sub.add_parser("clear", help="Remove a document")
|
| 92 |
-
p_clear.add_argument("source", help="Source filename to remove")
|
| 93 |
-
|
| 94 |
-
args = parser.parse_args()
|
| 95 |
-
|
| 96 |
-
if args.command == "ingest":
|
| 97 |
-
cmd_ingest(args.file)
|
| 98 |
-
elif args.command == "query":
|
| 99 |
-
cmd_query(args.question)
|
| 100 |
-
elif args.command == "list":
|
| 101 |
-
cmd_list()
|
| 102 |
-
elif args.command == "clear":
|
| 103 |
-
cmd_clear(args.source)
|
| 104 |
-
elif args.command == "status":
|
| 105 |
-
cmd_status()
|
| 106 |
-
else:
|
| 107 |
-
parser.print_help()
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
if __name__ == "__main__":
|
| 111 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
manage_db.py
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Manage HealthExpert databases.
|
| 3 |
-
|
| 4 |
-
Database plan:
|
| 5 |
-
┌─────────────────┬────────────────────────────────────────────────────┐
|
| 6 |
-
│ Database │ Management │
|
| 7 |
-
├─────────────────┼────────────────────────────────────────────────────┤
|
| 8 |
-
│ ChromaDB │ Embedded (in-process). No Docker container. │
|
| 9 |
-
│ (Vector Store) │ Data stored at: data/chroma_db/ │
|
| 10 |
-
│ │ Managed by: pipeline/vector_store.py │
|
| 11 |
-
│ │ Use /api/admin/purge (UI) or wipe data/chroma_db/ │
|
| 12 |
-
├─────────────────┼────────────────────────────────────────────────────┤
|
| 13 |
-
│ Kuzu │ Embedded (in-process). No Docker container. │
|
| 14 |
-
│ (Graph DB) │ Data stored at: data/kuzu_db/ │
|
| 15 |
-
│ │ Managed by: pipeline/graph_store.py │
|
| 16 |
-
│ │ Use /api/admin/purge (UI) or wipe data/kuzu_db/ │
|
| 17 |
-
└─────────────────┴────────────────────────────────────────────────────┘
|
| 18 |
-
|
| 19 |
-
Usage:
|
| 20 |
-
python manage_db.py # show status of all databases
|
| 21 |
-
python manage_db.py -chroma # show ChromaDB data directory info
|
| 22 |
-
python manage_db.py -kuzu # show Kuzu data directory info
|
| 23 |
-
"""
|
| 24 |
-
import argparse
|
| 25 |
-
import sys
|
| 26 |
-
import os
|
| 27 |
-
from pathlib import Path
|
| 28 |
-
|
| 29 |
-
CHROMA_DIR = Path(__file__).parent / "data" / "chroma_db"
|
| 30 |
-
KUZU_DIR = Path(__file__).parent / "data" / "kuzu_db"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def _dir_info(path: Path) -> dict:
|
| 34 |
-
exists = path.exists()
|
| 35 |
-
size_mb = 0.0
|
| 36 |
-
file_count = 0
|
| 37 |
-
if exists:
|
| 38 |
-
for f in path.rglob("*"):
|
| 39 |
-
if f.is_file():
|
| 40 |
-
size_mb += f.stat().st_size / 1024 ** 2
|
| 41 |
-
file_count += 1
|
| 42 |
-
return {"exists": exists, "path": str(path), "size_mb": size_mb, "files": file_count}
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def chroma_status() -> None:
|
| 46 |
-
info = _dir_info(CHROMA_DIR)
|
| 47 |
-
print("\n── ChromaDB (Vector Store) ──────────────────────────────────")
|
| 48 |
-
print(f" Type : Embedded (in-process, no server)")
|
| 49 |
-
print(f" Data dir : {info['path']}")
|
| 50 |
-
if info["exists"]:
|
| 51 |
-
print(f" Status : PRESENT ({info['files']} files, {info['size_mb']:.2f} MB)")
|
| 52 |
-
else:
|
| 53 |
-
print(f" Status : EMPTY (will be created on first ingest)")
|
| 54 |
-
print("─" * 60)
|
| 55 |
-
|
| 56 |
-
def kuzu_status() -> None:
|
| 57 |
-
info = _dir_info(KUZU_DIR)
|
| 58 |
-
print("\n── Kuzu (Graph DB) ──────────────────────────────────────────")
|
| 59 |
-
print(f" Type : Embedded (in-process, no server)")
|
| 60 |
-
print(f" Data dir : {info['path']}")
|
| 61 |
-
if info["exists"]:
|
| 62 |
-
print(f" Status : PRESENT ({info['files']} files, {info['size_mb']:.2f} MB)")
|
| 63 |
-
else:
|
| 64 |
-
print(f" Status : EMPTY (will be created on first ingest)")
|
| 65 |
-
print("─" * 60)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def status() -> None:
|
| 69 |
-
print("\n══════════════════════════════════════════════════════════════")
|
| 70 |
-
print(" HealthExpert — Database Status")
|
| 71 |
-
print("══════════════════════════════════════════════════════════════")
|
| 72 |
-
chroma_status()
|
| 73 |
-
kuzu_status()
|
| 74 |
-
print("")
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def main() -> None:
|
| 78 |
-
parser = argparse.ArgumentParser(
|
| 79 |
-
description="Manage HealthExpert databases",
|
| 80 |
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 81 |
-
epilog=__doc__,
|
| 82 |
-
)
|
| 83 |
-
parser.add_argument("-chroma", action="store_true", help="Show ChromaDB info")
|
| 84 |
-
parser.add_argument("-kuzu", action="store_true", help="Show Kuzu DB info")
|
| 85 |
-
args = parser.parse_args()
|
| 86 |
-
|
| 87 |
-
if args.chroma: chroma_status()
|
| 88 |
-
elif args.kuzu: kuzu_status()
|
| 89 |
-
else: status()
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
if __name__ == "__main__":
|
| 93 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
manage_llm.py
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Manage HealthExpert LLM Microservices.
|
| 3 |
-
|
| 4 |
-
This script allows you to spin up, spin down, and check the status
|
| 5 |
-
of the local LLM inference endpoints (gen_llm.py and embed_llm.py).
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
python manage_llm.py status # show status of LLM servers
|
| 9 |
-
python manage_llm.py up # start both LLM servers
|
| 10 |
-
python manage_llm.py down # stop both LLM servers
|
| 11 |
-
"""
|
| 12 |
-
import argparse
|
| 13 |
-
import subprocess
|
| 14 |
-
import time
|
| 15 |
-
import os
|
| 16 |
-
import sys
|
| 17 |
-
from pathlib import Path
|
| 18 |
-
import urllib.request
|
| 19 |
-
|
| 20 |
-
GEN_PORT = 8002
|
| 21 |
-
EMBED_PORT = 8003
|
| 22 |
-
BASE_DIR = Path(__file__).parent.resolve()
|
| 23 |
-
|
| 24 |
-
def _check_port(port: int) -> bool:
|
| 25 |
-
"""Check if a port is actively listening by making a simple HTTP request."""
|
| 26 |
-
try:
|
| 27 |
-
# Just a healthcheck to see if server responds, we expect 404 or 200
|
| 28 |
-
req = urllib.request.Request(f"http://127.0.0.1:{port}/")
|
| 29 |
-
urllib.request.urlopen(req, timeout=1)
|
| 30 |
-
return True
|
| 31 |
-
except urllib.error.URLError as e:
|
| 32 |
-
# If it's an HTTPError (e.g. 404 Not Found), the server is alive
|
| 33 |
-
if hasattr(e, 'code'):
|
| 34 |
-
return True
|
| 35 |
-
# ConnectionRefusedError usually means nothing is listening
|
| 36 |
-
return False
|
| 37 |
-
except Exception:
|
| 38 |
-
return False
|
| 39 |
-
|
| 40 |
-
def _kill_port(port: int) -> None:
|
| 41 |
-
"""Kill any process listening on the given port."""
|
| 42 |
-
try:
|
| 43 |
-
# Using fuser to kill processes on the port
|
| 44 |
-
subprocess.run(["fuser", "-k", f"{port}/tcp"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 45 |
-
except FileNotFoundError:
|
| 46 |
-
try:
|
| 47 |
-
# Fallback to lsof if fuser is not available
|
| 48 |
-
pids = subprocess.check_output(["lsof", "-t", f"-i:{port}"]).decode().strip().split('\n')
|
| 49 |
-
for pid in pids:
|
| 50 |
-
if pid:
|
| 51 |
-
subprocess.run(["kill", "-9", pid])
|
| 52 |
-
except Exception:
|
| 53 |
-
pass
|
| 54 |
-
|
| 55 |
-
def status() -> None:
|
| 56 |
-
"""Show the status of the LLM servers."""
|
| 57 |
-
gen_alive = _check_port(GEN_PORT)
|
| 58 |
-
embed_alive = _check_port(EMBED_PORT)
|
| 59 |
-
|
| 60 |
-
print("\n══════════════════════════════════════════════════════════════")
|
| 61 |
-
print(" HealthExpert — LLM Microservices Status")
|
| 62 |
-
print("══════════════════════════════════════════════════════════════")
|
| 63 |
-
|
| 64 |
-
print("\n── Generation LLM (agents/gen_llm.py) ───────────────────────")
|
| 65 |
-
print(f" Port : {GEN_PORT}")
|
| 66 |
-
print(f" Status : {'🟢 RUNNING' if gen_alive else '🔴 STOPPED'}")
|
| 67 |
-
|
| 68 |
-
print("\n── Embedding LLM (agents/embed_llm.py) ──────────────────────")
|
| 69 |
-
print(f" Port : {EMBED_PORT}")
|
| 70 |
-
print(f" Status : {'🟢 RUNNING' if embed_alive else '🔴 STOPPED'}")
|
| 71 |
-
print("─" * 62)
|
| 72 |
-
print("")
|
| 73 |
-
|
| 74 |
-
def down() -> None:
|
| 75 |
-
"""Stop the LLM servers."""
|
| 76 |
-
print("Stopping LLM services...")
|
| 77 |
-
_kill_port(GEN_PORT)
|
| 78 |
-
_kill_port(EMBED_PORT)
|
| 79 |
-
|
| 80 |
-
# Also kill by script name as a fallback
|
| 81 |
-
subprocess.run(["pkill", "-f", "agents/gen_llm.py"], stderr=subprocess.DEVNULL)
|
| 82 |
-
subprocess.run(["pkill", "-f", "agents/embed_llm.py"], stderr=subprocess.DEVNULL)
|
| 83 |
-
|
| 84 |
-
time.sleep(1)
|
| 85 |
-
print("LLM services stopped.")
|
| 86 |
-
|
| 87 |
-
def up(hf_mode: bool = False) -> None:
|
| 88 |
-
"""Start the LLM servers."""
|
| 89 |
-
gen_alive = _check_port(GEN_PORT)
|
| 90 |
-
embed_alive = _check_port(EMBED_PORT)
|
| 91 |
-
|
| 92 |
-
if gen_alive and embed_alive:
|
| 93 |
-
print("Both LLM services are already running.")
|
| 94 |
-
return
|
| 95 |
-
|
| 96 |
-
print("Starting LLM services...")
|
| 97 |
-
|
| 98 |
-
env = os.environ.copy()
|
| 99 |
-
if hf_mode:
|
| 100 |
-
env["HF_MODE"] = "1"
|
| 101 |
-
print("Running in HF CPU mode (HF_MODE=1)")
|
| 102 |
-
|
| 103 |
-
# Start Embed LLM
|
| 104 |
-
if not embed_alive:
|
| 105 |
-
print(f"[1/2] Starting embed_llm on port {EMBED_PORT}...")
|
| 106 |
-
subprocess.Popen(
|
| 107 |
-
[sys.executable, str(BASE_DIR / "agents" / "embed_llm.py")],
|
| 108 |
-
cwd=BASE_DIR,
|
| 109 |
-
stdout=subprocess.DEVNULL,
|
| 110 |
-
stderr=subprocess.DEVNULL,
|
| 111 |
-
start_new_session=True,
|
| 112 |
-
env=env
|
| 113 |
-
)
|
| 114 |
-
else:
|
| 115 |
-
print(f"[1/2] embed_llm is already running on port {EMBED_PORT}.")
|
| 116 |
-
|
| 117 |
-
# Start Gen LLM
|
| 118 |
-
if not gen_alive:
|
| 119 |
-
print(f"[2/2] Starting gen_llm on port {GEN_PORT}...")
|
| 120 |
-
subprocess.Popen(
|
| 121 |
-
[sys.executable, str(BASE_DIR / "agents" / "gen_llm.py")],
|
| 122 |
-
cwd=BASE_DIR,
|
| 123 |
-
stdout=subprocess.DEVNULL,
|
| 124 |
-
stderr=subprocess.DEVNULL,
|
| 125 |
-
start_new_session=True,
|
| 126 |
-
env=env
|
| 127 |
-
)
|
| 128 |
-
else:
|
| 129 |
-
print(f"[2/2] gen_llm is already running on port {GEN_PORT}.")
|
| 130 |
-
|
| 131 |
-
print("\nWaiting for services to initialize...")
|
| 132 |
-
time.sleep(3)
|
| 133 |
-
status()
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def main() -> None:
|
| 137 |
-
parser = argparse.ArgumentParser(
|
| 138 |
-
description="Manage HealthExpert LLM microservices",
|
| 139 |
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 140 |
-
epilog=__doc__,
|
| 141 |
-
)
|
| 142 |
-
|
| 143 |
-
# Optional positional argument for the command
|
| 144 |
-
parser.add_argument("command", nargs="?", choices=["up", "down", "status"], default="status",
|
| 145 |
-
help="Action to perform (default: status)")
|
| 146 |
-
parser.add_argument("-hf", "--hf", action="store_true", help="Start servers in HuggingFace/CPU mode")
|
| 147 |
-
|
| 148 |
-
args = parser.parse_args()
|
| 149 |
-
|
| 150 |
-
if args.command == "up":
|
| 151 |
-
up(hf_mode=args.hf)
|
| 152 |
-
elif args.command == "down":
|
| 153 |
-
down()
|
| 154 |
-
else:
|
| 155 |
-
status()
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
if __name__ == "__main__":
|
| 159 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pytest.ini
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
[pytest]
|
| 2 |
-
testpaths = tests
|
| 3 |
-
python_files = test_*.py
|
| 4 |
-
python_classes = Test*
|
| 5 |
-
python_functions = test_*
|
| 6 |
-
markers =
|
| 7 |
-
unit: Unit tests for individual components
|
| 8 |
-
integration: Integration tests for workflows
|
| 9 |
-
slow: Tests that take longer than 1 second
|
| 10 |
-
requires_gpu: Tests that require GPU
|
| 11 |
-
requires_services: Tests that require running services
|
| 12 |
-
addopts =
|
| 13 |
-
-v
|
| 14 |
-
--tb=short
|
| 15 |
-
--strict-markers
|
| 16 |
-
-ra
|
| 17 |
-
minversion = 7.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|