nothingworry commited on
Commit
0452a50
·
1 Parent(s): da3f5f6

Add Docker support and remove Ollama

Browse files
DOCKER_COMMANDS.md ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Commands for IntegraChat
2
+
3
+ ## Quick Start Commands
4
+
5
+ ### 1. Stop and Remove Existing Container
6
+ ```powershell
7
+ docker rm -f integrachat
8
+ ```
9
+
10
+ ### 2. Build the Docker Image
11
+ ```powershell
12
+ docker build -t integrachat:latest .
13
+ ```
14
+
15
+ ### 3. Run the Container
16
+ ```powershell
17
+ docker run -d --name integrachat `
18
+ -p 7860:7860 `
19
+ -p 8000:8000 `
20
+ -p 8900:8900 `
21
+ --env-file .env `
22
+ -e DOCKER_CONTAINER=1 `
23
+ integrachat:latest
24
+ ```
25
+
26
+ ### 4. View Logs
27
+ ```powershell
28
+ # Follow logs (live)
29
+ docker logs -f integrachat
30
+
31
+ # Last 50 lines
32
+ docker logs --tail 50 integrachat
33
+
34
+ # Last 100 lines with timestamps
35
+ docker logs --tail 100 -t integrachat
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Container Management
41
+
42
+ ### Check Container Status
43
+ ```powershell
44
+ # Check if running
45
+ docker ps --filter "name=integrachat"
46
+
47
+ # Check all containers (including stopped)
48
+ docker ps -a --filter "name=integrachat"
49
+
50
+ # Detailed status
51
+ docker ps --filter "name=integrachat" --format "Container: {{.Names}} | Status: {{.Status}} | Ports: {{.Ports}}"
52
+ ```
53
+
54
+ ### Stop Container
55
+ ```powershell
56
+ docker stop integrachat
57
+ ```
58
+
59
+ ### Start Container (if stopped)
60
+ ```powershell
61
+ docker start integrachat
62
+ ```
63
+
64
+ ### Restart Container
65
+ ```powershell
66
+ docker restart integrachat
67
+ ```
68
+
69
+ ### Remove Container
70
+ ```powershell
71
+ # Stop and remove
72
+ docker rm -f integrachat
73
+
74
+ # Remove only if stopped
75
+ docker rm integrachat
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Image Management
81
+
82
+ ### List Images
83
+ ```powershell
84
+ docker images integrachat
85
+ ```
86
+
87
+ ### Remove Image
88
+ ```powershell
89
+ docker rmi integrachat:latest
90
+ ```
91
+
92
+ ### Rebuild Without Cache
93
+ ```powershell
94
+ docker build --no-cache -t integrachat:latest .
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Debugging Commands
100
+
101
+ ### Execute Commands Inside Container
102
+ ```powershell
103
+ # Open shell in container
104
+ docker exec -it integrachat /bin/bash
105
+
106
+ # Run Python command
107
+ docker exec integrachat python --version
108
+
109
+ # Check environment variables
110
+ docker exec integrachat printenv | Select-String -Pattern "GROQ|MCP|API"
111
+
112
+ # Check if services are running
113
+ docker exec integrachat ps aux
114
+ ```
115
+
116
+ ### Check Service Health
117
+ ```powershell
118
+ # Check FastAPI health
119
+ docker exec integrachat curl -s http://localhost:8000/health
120
+
121
+ # Check MCP server health
122
+ docker exec integrachat curl -s http://localhost:8900/health
123
+
124
+ # Check Gradio (from host)
125
+ Invoke-WebRequest -Uri http://localhost:7860 -UseBasicParsing -TimeoutSec 5
126
+ ```
127
+
128
+ ### View Service Logs
129
+ ```powershell
130
+ # FastAPI logs
131
+ docker exec integrachat tail -n 50 /app/logs/fastapi.log
132
+
133
+ # MCP server logs
134
+ docker exec integrachat tail -n 50 /app/logs/mcp.log
135
+
136
+ # Gradio logs
137
+ docker exec integrachat tail -n 50 /app/logs/gradio.log
138
+
139
+ # All logs
140
+ docker exec integrachat tail -n 50 /app/logs/*.log
141
+ ```
142
+
143
+ ### Check Ports
144
+ ```powershell
145
+ # Check what ports are mapped
146
+ docker port integrachat
147
+
148
+ # Check if ports are listening (from host)
149
+ netstat -an | Select-String -Pattern "7860|8000|8900"
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Complete Rebuild Sequence
155
+
156
+ ```powershell
157
+ # 1. Stop and remove container
158
+ docker rm -f integrachat
159
+
160
+ # 2. Remove old image (optional)
161
+ docker rmi integrachat:latest
162
+
163
+ # 3. Build new image
164
+ docker build -t integrachat:latest .
165
+
166
+ # 4. Run container
167
+ docker run -d --name integrachat `
168
+ -p 7860:7860 `
169
+ -p 8000:8000 `
170
+ -p 8900:8900 `
171
+ --env-file .env `
172
+ -e DOCKER_CONTAINER=1 `
173
+ integrachat:latest
174
+
175
+ # 5. Check status
176
+ docker ps --filter "name=integrachat"
177
+
178
+ # 6. View logs
179
+ docker logs -f integrachat
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Quick Health Check
185
+
186
+ ```powershell
187
+ # Check all services
188
+ Write-Host "Container Status:" -ForegroundColor Cyan
189
+ docker ps --filter "name=integrachat" --format " {{.Names}}: {{.Status}}"
190
+
191
+ Write-Host "`nService Health:" -ForegroundColor Cyan
192
+ Write-Host " FastAPI:" -NoNewline
193
+ docker exec integrachat curl -s http://localhost:8000/health 2>&1 | Out-Null
194
+ if ($LASTEXITCODE -eq 0) { Write-Host " ✓ Running" -ForegroundColor Green } else { Write-Host " ✗ Not responding" -ForegroundColor Red }
195
+
196
+ Write-Host " MCP Server:" -NoNewline
197
+ docker exec integrachat curl -s http://localhost:8900/health 2>&1 | Out-Null
198
+ if ($LASTEXITCODE -eq 0) { Write-Host " ✓ Running" -ForegroundColor Green } else { Write-Host " ✗ Not responding" -ForegroundColor Red }
199
+
200
+ Write-Host " Gradio UI:" -NoNewline
201
+ try { $response = Invoke-WebRequest -Uri http://localhost:7860 -UseBasicParsing -TimeoutSec 2; Write-Host " ✓ Running" -ForegroundColor Green } catch { Write-Host " ✗ Not responding" -ForegroundColor Red }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Access URLs
207
+
208
+ Once the container is running, access:
209
+
210
+ - **Gradio UI**: http://localhost:7860
211
+ - **FastAPI API**: http://localhost:8000
212
+ - **API Docs**: http://localhost:8000/docs
213
+ - **MCP Server**: http://localhost:8900
214
+ - **MCP Server Docs**: http://localhost:8900/docs
215
+
216
+ ---
217
+
218
+ ## Troubleshooting
219
+
220
+ ### Container won't start
221
+ ```powershell
222
+ # Check logs for errors
223
+ docker logs integrachat
224
+
225
+ # Check if ports are already in use
226
+ netstat -an | Select-String -Pattern "7860|8000|8900"
227
+ ```
228
+
229
+ ### Services not responding
230
+ ```powershell
231
+ # Restart container
232
+ docker restart integrachat
233
+
234
+ # Check service logs inside container
235
+ docker exec integrachat tail -n 100 /app/logs/*.log
236
+ ```
237
+
238
+ ### Clear everything and start fresh
239
+ ```powershell
240
+ # Stop and remove container
241
+ docker rm -f integrachat
242
+
243
+ # Remove image
244
+ docker rmi integrachat:latest
245
+
246
+ # Clear build cache (optional)
247
+ docker builder prune -f
248
+
249
+ # Rebuild from scratch
250
+ docker build --no-cache -t integrachat:latest .
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Environment Variables
256
+
257
+ Make sure your `.env` file has:
258
+ - `GROQ_API_KEY` - Your Groq API key
259
+ - `GROQ_MODEL` - Model name (default: llama-3.1-8b-instant)
260
+ - `RAG_MCP_URL` - http://localhost:8900/rag
261
+ - `WEB_MCP_URL` - http://localhost:8900/web
262
+ - `ADMIN_MCP_URL` - http://localhost:8900/admin
263
+ - `MCP_PORT` - 8900
264
+ - `API_PORT` - 8000
265
+ - `POSTGRESQL_URL` - Your database connection string (optional)
266
+
DOCKER_GUIDE.md ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Setup Guide for IntegraChat
2
+
3
+ ## Quick Start
4
+
5
+ ### Option 1: Use PowerShell Script (Easiest for Windows)
6
+
7
+ ```powershell
8
+ # Run the helper script
9
+ .\run-docker.ps1
10
+ ```
11
+
12
+ ### Option 2: Build and Run with Docker Compose (Recommended)
13
+
14
+ ```powershell
15
+ # PowerShell
16
+ docker-compose up -d
17
+
18
+ # Or with rebuild
19
+ docker-compose up -d --build
20
+ ```
21
+
22
+ ### Build and Run Manually
23
+
24
+ **PowerShell (Windows):**
25
+ ```powershell
26
+ # Build the image
27
+ docker build -t integrachat:latest .
28
+
29
+ # Run the container (PowerShell uses backticks for line continuation)
30
+ docker run -d --name integrachat `
31
+ -p 7860:7860 -p 8000:8000 -p 8900:8900 `
32
+ -e DOCKER_CONTAINER=1 `
33
+ integrachat:latest
34
+
35
+ # Or use a single line:
36
+ docker run -d --name integrachat -p 7860:7860 -p 8000:8000 -p 8900:8900 -e DOCKER_CONTAINER=1 integrachat:latest
37
+ ```
38
+
39
+ **Bash/Linux/Mac:**
40
+ ```bash
41
+ # Build the image
42
+ docker build -t integrachat:latest .
43
+
44
+ # Run the container
45
+ docker run -d --name integrachat \
46
+ -p 7860:7860 -p 8000:8000 -p 8900:8900 \
47
+ -e DOCKER_CONTAINER=1 \
48
+ integrachat:latest
49
+ ```
50
+
51
+ ## Container Management
52
+
53
+ ### View Logs
54
+ ```bash
55
+ # All logs (streaming)
56
+ docker logs -f integrachat
57
+
58
+ # Specific service logs
59
+ docker exec integrachat tail -f /app/logs/fastapi.log
60
+ docker exec integrachat tail -f /app/logs/gradio.log
61
+ docker exec integrachat tail -f /app/logs/mcp.log
62
+ ```
63
+
64
+ ### Stop Container
65
+ ```bash
66
+ docker stop integrachat
67
+ ```
68
+
69
+ ### Start Container
70
+ ```bash
71
+ docker start integrachat
72
+ ```
73
+
74
+ ### Remove Container
75
+ ```bash
76
+ docker stop integrachat
77
+ docker rm integrachat
78
+ ```
79
+
80
+ ### Rebuild After Changes
81
+
82
+ **PowerShell:**
83
+ ```powershell
84
+ docker stop integrachat
85
+ docker rm integrachat
86
+ docker build -t integrachat:latest .
87
+ docker run -d --name integrachat -p 7860:7860 -p 8000:8000 -p 8900:8900 -e DOCKER_CONTAINER=1 integrachat:latest
88
+ ```
89
+
90
+ **Bash:**
91
+ ```bash
92
+ docker stop integrachat
93
+ docker rm integrachat
94
+ docker build -t integrachat:latest .
95
+ docker run -d --name integrachat \
96
+ -p 7860:7860 -p 8000:8000 -p 8900:8900 \
97
+ -e DOCKER_CONTAINER=1 \
98
+ integrachat:latest
99
+ ```
100
+
101
+ ## Access Services
102
+
103
+ - **Gradio UI**: http://localhost:7860
104
+ - **FastAPI API**: http://localhost:8000
105
+ - **MCP Server**: http://localhost:8900
106
+ - **API Docs**: http://localhost:8000/docs
107
+ - **MCP Docs**: http://localhost:8900/docs
108
+
109
+ ## Environment Variables
110
+
111
+ Create a `.env` file (or use docker-compose.yml) to configure:
112
+
113
+ ```env
114
+ # LLM Configuration
115
+ LLM_BACKEND=groq # or "ollama"
116
+ GROQ_API_KEY=your_key_here
117
+ GROQ_MODEL=llama-3.1-8b-instant
118
+
119
+ # Supabase (optional - for analytics)
120
+ SUPABASE_URL=https://your-project.supabase.co
121
+ SUPABASE_SERVICE_KEY=your_service_key
122
+
123
+ # Ports (defaults shown)
124
+ API_PORT=8000
125
+ MCP_PORT=8900
126
+ GRADIO_PORT=7860
127
+ ```
128
+
129
+ ## Docker Compose
130
+
131
+ The `docker-compose.yml` file provides:
132
+ - Easy service management
133
+ - Environment variable support
134
+ - Volume mounting for logs
135
+ - Health checks
136
+ - Auto-restart on failure
137
+
138
+ ### Using Docker Compose
139
+
140
+ ```bash
141
+ # Start services
142
+ docker-compose up -d
143
+
144
+ # View logs
145
+ docker-compose logs -f
146
+
147
+ # Stop services
148
+ docker-compose down
149
+
150
+ # Rebuild and restart
151
+ docker-compose up -d --build
152
+ ```
153
+
154
+ ## PowerShell-Specific Notes
155
+
156
+ ### Line Continuation
157
+ PowerShell uses backticks (`` ` ``) for line continuation, not backslashes (`\`):
158
+
159
+ ```powershell
160
+ # ✅ Correct (PowerShell)
161
+ docker run -d --name integrachat `
162
+ -p 7860:7860 `
163
+ -p 8000:8000 `
164
+ integrachat:latest
165
+
166
+ # ✅ Also correct (single line)
167
+ docker run -d --name integrachat -p 7860:7860 -p 8000:8000 -p 8900:8900 -e DOCKER_CONTAINER=1 integrachat:latest
168
+
169
+ # ❌ Wrong (bash syntax - doesn't work in PowerShell)
170
+ docker run -d --name integrachat \
171
+ -p 7860:7860 \
172
+ integrachat:latest
173
+ ```
174
+
175
+ ### Quick Commands Script
176
+ Use `run-docker.ps1` for easy container management:
177
+ ```powershell
178
+ .\run-docker.ps1
179
+ ```
180
+
181
+ ## Troubleshooting
182
+
183
+ ### Check Container Status
184
+ ```bash
185
+ docker ps -a | grep integrachat
186
+ ```
187
+
188
+ ### Check Service Health
189
+ ```bash
190
+ # FastAPI health
191
+ curl http://localhost:8000/health
192
+
193
+ # MCP health
194
+ curl http://localhost:8900/health
195
+ ```
196
+
197
+ ### View All Logs
198
+ ```bash
199
+ docker exec integrachat tail -n 100 /app/logs/*.log
200
+ ```
201
+
202
+ ### Restart Services Inside Container
203
+ ```bash
204
+ # Container will auto-restart services, but you can manually restart:
205
+ docker restart integrachat
206
+ ```
207
+
208
+ ### Clean Up
209
+ ```bash
210
+ # Remove container and image
211
+ docker stop integrachat
212
+ docker rm integrachat
213
+ docker rmi integrachat:latest
214
+
215
+ # Remove all unused Docker resources
216
+ docker system prune -a
217
+ ```
218
+
219
+ ## Notes
220
+
221
+ - The container runs all three services (MCP, FastAPI, Gradio) automatically
222
+ - Logs are written to `/app/logs/` inside the container
223
+ - The entrypoint script handles service startup and health checks
224
+ - Supabase warnings are expected if credentials are not configured (analytics will be disabled gracefully)
225
+
Dockerfile CHANGED
@@ -8,6 +8,7 @@ WORKDIR /app
8
  # Install system dependencies
9
  RUN apt-get update && \
10
  apt-get install -y --no-install-recommends \
 
11
  build-essential \
12
  curl \
13
  git && \
 
8
  # Install system dependencies
9
  RUN apt-get update && \
10
  apt-get install -y --no-install-recommends \
11
+ --fix-missing \
12
  build-essential \
13
  curl \
14
  git && \
backend/api/mcp_clients/rag_client.py CHANGED
@@ -11,7 +11,7 @@ class RAGClient:
11
  """
12
 
13
  def __init__(self):
14
- self.base_url = os.getenv("RAG_MCP_URL", "http://localhost:8001")
15
  if not self.base_url:
16
  raise ValueError("RAG_MCP_URL environment variable is not set")
17
  self.search_endpoint = f"{self.base_url}/search"
 
11
  """
12
 
13
  def __init__(self):
14
+ self.base_url = os.getenv("RAG_MCP_URL", "http://localhost:8900/rag")
15
  if not self.base_url:
16
  raise ValueError("RAG_MCP_URL environment variable is not set")
17
  self.search_endpoint = f"{self.base_url}/search"
backend/api/routes/admin.py CHANGED
@@ -39,11 +39,16 @@ def _get_analytics_store() -> Optional[AnalyticsStore]:
39
  try:
40
  _analytics_store = AnalyticsStore()
41
  except RuntimeError as exc:
42
- logger.warning("Admin analytics disabled: %s", exc)
 
 
 
 
 
43
  _analytics_failed = True
44
  _analytics_store = None
45
  except Exception as exc: # pragma: no cover - unexpected failures
46
- logger.debug("Admin analytics unexpected init failure: %s", exc)
47
  _analytics_failed = True
48
  _analytics_store = None
49
 
 
39
  try:
40
  _analytics_store = AnalyticsStore()
41
  except RuntimeError as exc:
42
+ # Only log at warning level if credentials are configured (actual error)
43
+ # Otherwise log at debug level (expected when Supabase is not configured)
44
+ if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"):
45
+ logger.warning("Analytics disabled: %s", str(exc).split('\n')[0]) # Only first line
46
+ else:
47
+ logger.debug("Analytics disabled: %s", str(exc).split('\n')[0])
48
  _analytics_failed = True
49
  _analytics_store = None
50
  except Exception as exc: # pragma: no cover - unexpected failures
51
+ logger.debug("Analytics unexpected init failure: %s", exc)
52
  _analytics_failed = True
53
  _analytics_store = None
54
 
backend/api/routes/agent.py CHANGED
@@ -23,10 +23,9 @@ router = APIRouter()
23
 
24
 
25
  orchestrator = AgentOrchestrator(
26
- rag_mcp_url=os.getenv("RAG_MCP_URL", "http://localhost:8001"),
27
- web_mcp_url=os.getenv("WEB_MCP_URL", "http://localhost:8002"),
28
- admin_mcp_url=os.getenv("ADMIN_MCP_URL", "http://localhost:8003"),
29
- llm_backend=os.getenv("LLM_BACKEND", "ollama")
30
  )
31
 
32
 
 
23
 
24
 
25
  orchestrator = AgentOrchestrator(
26
+ rag_mcp_url=os.getenv("RAG_MCP_URL", "http://localhost:8900/rag"),
27
+ web_mcp_url=os.getenv("WEB_MCP_URL", "http://localhost:8900/web"),
28
+ admin_mcp_url=os.getenv("ADMIN_MCP_URL", "http://localhost:8900/admin")
 
29
  )
30
 
31
 
backend/api/routes/analytics.py CHANGED
@@ -19,17 +19,24 @@ try:
19
  analytics_store: Optional[AnalyticsStore] = AnalyticsStore()
20
  else:
21
  analytics_store = None
22
- logger.warning(
23
  "AnalyticsStore: Supabase credentials not configured. "
24
  "Analytics endpoints will return 503."
25
  )
26
  except RuntimeError as exc:
27
  analytics_store = None
28
- logger.warning(
29
- "AnalyticsStore initialization failed (%s). "
30
- "Analytics endpoints will return 503.",
31
- exc,
32
- )
 
 
 
 
 
 
 
33
 
34
 
35
  @router.get("/overview")
@@ -43,16 +50,30 @@ async def analytics_overview(
43
  Includes total queries, tool usage, red-flag count, and active users.
44
  """
45
 
46
- if analytics_store is None:
47
- raise HTTPException(
48
- status_code=503,
49
- detail="Analytics is disabled because Supabase is not configured on this deployment.",
50
- )
51
-
52
  if not x_tenant_id:
53
  raise HTTPException(status_code=400, detail="Missing tenant ID")
54
  require_api_permission(x_user_role, "view_analytics")
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
57
 
58
  tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp)
@@ -83,16 +104,18 @@ async def analytics_tool_usage(
83
  Includes counts, latency, tokens, and success/error rates.
84
  """
85
 
86
- if analytics_store is None:
87
- raise HTTPException(
88
- status_code=503,
89
- detail="Analytics is disabled because Supabase is not configured on this deployment.",
90
- )
91
-
92
  if not x_tenant_id:
93
  raise HTTPException(status_code=400, detail="Missing tenant ID")
94
  require_api_permission(x_user_role, "view_analytics")
95
 
 
 
 
 
 
 
 
 
96
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
97
  tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp)
98
 
@@ -115,16 +138,18 @@ async def analytics_redflags(
115
  Includes rule details, severity, confidence, and timestamps.
116
  """
117
 
118
- if analytics_store is None:
119
- raise HTTPException(
120
- status_code=503,
121
- detail="Analytics is disabled because Supabase is not configured on this deployment.",
122
- )
123
-
124
  if not x_tenant_id:
125
  raise HTTPException(status_code=400, detail="Missing tenant ID")
126
  require_api_permission(x_user_role, "view_analytics")
127
 
 
 
 
 
 
 
 
 
128
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
129
  redflags = analytics_store.get_redflag_violations(x_tenant_id, limit, since_timestamp)
130
 
@@ -151,16 +176,24 @@ async def analytics_activity(
151
  Includes total queries, active users, last query timestamp, and individual activity records for heatmap visualization.
152
  """
153
 
154
- if analytics_store is None:
155
- raise HTTPException(
156
- status_code=503,
157
- detail="Analytics is disabled because Supabase is not configured on this deployment.",
158
- )
159
-
160
  if not x_tenant_id:
161
  raise HTTPException(status_code=400, detail="Missing tenant ID")
162
  require_api_permission(x_user_role, "view_analytics")
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
165
  activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp)
166
 
@@ -186,16 +219,24 @@ async def analytics_rag_quality(
186
  Includes average hits, scores, and latency.
187
  """
188
 
189
- if analytics_store is None:
190
- raise HTTPException(
191
- status_code=503,
192
- detail="Analytics is disabled because Supabase is not configured on this deployment.",
193
- )
194
-
195
  if not x_tenant_id:
196
  raise HTTPException(status_code=400, detail="Missing tenant ID")
197
  require_api_permission(x_user_role, "view_analytics")
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
200
  rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp)
201
 
 
19
  analytics_store: Optional[AnalyticsStore] = AnalyticsStore()
20
  else:
21
  analytics_store = None
22
+ logger.debug(
23
  "AnalyticsStore: Supabase credentials not configured. "
24
  "Analytics endpoints will return 503."
25
  )
26
  except RuntimeError as exc:
27
  analytics_store = None
28
+ # Only log at warning level if credentials are configured (actual error)
29
+ # Otherwise log at debug level (expected when Supabase is not configured)
30
+ if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"):
31
+ logger.warning(
32
+ "AnalyticsStore initialization failed (%s). Analytics endpoints will return 503.",
33
+ str(exc).split('\n')[0], # Only first line
34
+ )
35
+ else:
36
+ logger.debug(
37
+ "AnalyticsStore not configured (%s). Analytics endpoints will return 503.",
38
+ str(exc).split('\n')[0],
39
+ )
40
 
41
 
42
  @router.get("/overview")
 
50
  Includes total queries, tool usage, red-flag count, and active users.
51
  """
52
 
 
 
 
 
 
 
53
  if not x_tenant_id:
54
  raise HTTPException(status_code=400, detail="Missing tenant ID")
55
  require_api_permission(x_user_role, "view_analytics")
56
 
57
+ # Return empty data if analytics is not configured (instead of 503)
58
+ if analytics_store is None:
59
+ return {
60
+ "tenant_id": x_tenant_id,
61
+ "overview": {
62
+ "total_queries": 0,
63
+ "tool_usage": {},
64
+ "redflag_count": 0,
65
+ "active_users": 0,
66
+ "last_query": None,
67
+ "rag_quality": {
68
+ "total_searches": 0,
69
+ "avg_hits_per_search": 0,
70
+ "avg_score": 0.0,
71
+ "avg_top_score": 0.0,
72
+ "avg_latency_ms": 0.0
73
+ }
74
+ }
75
+ }
76
+
77
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
78
 
79
  tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp)
 
104
  Includes counts, latency, tokens, and success/error rates.
105
  """
106
 
 
 
 
 
 
 
107
  if not x_tenant_id:
108
  raise HTTPException(status_code=400, detail="Missing tenant ID")
109
  require_api_permission(x_user_role, "view_analytics")
110
 
111
+ # Return empty data if analytics is not configured (instead of 503)
112
+ if analytics_store is None:
113
+ return {
114
+ "tenant_id": x_tenant_id,
115
+ "tool_usage": {},
116
+ "period_days": days
117
+ }
118
+
119
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
120
  tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp)
121
 
 
138
  Includes rule details, severity, confidence, and timestamps.
139
  """
140
 
 
 
 
 
 
 
141
  if not x_tenant_id:
142
  raise HTTPException(status_code=400, detail="Missing tenant ID")
143
  require_api_permission(x_user_role, "view_analytics")
144
 
145
+ # Return empty data if analytics is not configured (instead of 503)
146
+ if analytics_store is None:
147
+ return {
148
+ "tenant_id": x_tenant_id,
149
+ "redflags": [],
150
+ "count": 0
151
+ }
152
+
153
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
154
  redflags = analytics_store.get_redflag_violations(x_tenant_id, limit, since_timestamp)
155
 
 
176
  Includes total queries, active users, last query timestamp, and individual activity records for heatmap visualization.
177
  """
178
 
 
 
 
 
 
 
179
  if not x_tenant_id:
180
  raise HTTPException(status_code=400, detail="Missing tenant ID")
181
  require_api_permission(x_user_role, "view_analytics")
182
 
183
+ # Return empty data if analytics is not configured (instead of 503)
184
+ if analytics_store is None:
185
+ return {
186
+ "tenant_id": x_tenant_id,
187
+ "activity": {
188
+ "total_queries": 0,
189
+ "active_users": 0,
190
+ "redflag_count": 0,
191
+ "last_query": None
192
+ },
193
+ "activities": [],
194
+ "period_days": days
195
+ }
196
+
197
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
198
  activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp)
199
 
 
219
  Includes average hits, scores, and latency.
220
  """
221
 
 
 
 
 
 
 
222
  if not x_tenant_id:
223
  raise HTTPException(status_code=400, detail="Missing tenant ID")
224
  require_api_permission(x_user_role, "view_analytics")
225
 
226
+ # Return empty data if analytics is not configured (instead of 503)
227
+ if analytics_store is None:
228
+ return {
229
+ "tenant_id": x_tenant_id,
230
+ "rag_quality": {
231
+ "total_searches": 0,
232
+ "avg_hits_per_search": 0,
233
+ "avg_score": 0.0,
234
+ "avg_top_score": 0.0,
235
+ "avg_latency_ms": 0.0
236
+ },
237
+ "period_days": days
238
+ }
239
+
240
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
241
  rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp)
242
 
backend/api/routes/rag.py CHANGED
@@ -261,7 +261,7 @@ async def rag_ingest_document(
261
  error_msg = (
262
  f"RAG server error: {error_detail}\n\n"
263
  f"Please check:\n"
264
- f"1. RAG_MCP_URL is set correctly (default: http://localhost:8001)\n"
265
  f"2. RAG MCP server is running\n"
266
  f"3. Database connection (POSTGRESQL_URL) is configured in the RAG server"
267
  )
 
261
  error_msg = (
262
  f"RAG server error: {error_detail}\n\n"
263
  f"Please check:\n"
264
+ f"1. RAG_MCP_URL is set correctly (default: http://localhost:8900/rag)\n"
265
  f"2. RAG MCP server is running\n"
266
  f"3. Database connection (POSTGRESQL_URL) is configured in the RAG server"
267
  )
backend/api/services/agent_orchestrator.py CHANGED
@@ -40,9 +40,10 @@ load_dotenv()
40
 
41
  class AgentOrchestrator:
42
 
43
- def __init__(self, rag_mcp_url: str, web_mcp_url: str, admin_mcp_url: str, llm_backend: str = "ollama"):
44
  self.mcp = MCPClient(rag_mcp_url, web_mcp_url, admin_mcp_url)
45
- self.llm = LLMClient(backend=llm_backend, url=os.getenv("OLLAMA_URL"), api_key=os.getenv("GROQ_API_KEY"), model=os.getenv("OLLAMA_MODEL"))
 
46
 
47
  # pass admin_mcp_url so detector can call back
48
  self.redflag = RedFlagDetector(
@@ -68,15 +69,20 @@ class AgentOrchestrator:
68
  return
69
 
70
  if self._analytics_disabled:
71
- print("⚠️ AgentOrchestrator Analytics: Disabled via ANALYTICS_DISABLED")
72
  else:
73
  store = self._get_analytics()
74
  if store is None:
75
- print("⚠️ AgentOrchestrator Analytics: Disabled (Supabase not configured)")
 
 
 
 
 
76
  elif store.use_supabase:
77
- print("✅ AgentOrchestrator Analytics: Using Supabase backend")
78
  else:
79
- print("⚠️ AgentOrchestrator Analytics: Using fallback backend")
80
 
81
  AgentOrchestrator._analytics_backend_logged = True
82
 
@@ -90,11 +96,17 @@ class AgentOrchestrator:
90
  try:
91
  self._analytics = AnalyticsStore()
92
  except RuntimeError as exc:
93
- logger.warning("AgentOrchestrator analytics disabled: %s", exc)
 
 
 
 
 
 
94
  self._analytics_failed = True
95
  self._analytics = None
96
  except Exception as exc: # pragma: no cover - unexpected initialization failures
97
- logger.debug("AgentOrchestrator analytics unexpected init failure: %s", exc)
98
  self._analytics_failed = True
99
  self._analytics = None
100
 
@@ -1169,14 +1181,13 @@ Answer:"""
1169
  fallback = await self.llm.simple_call(req.message, temperature=req.temperature)
1170
  except Exception as llm_error:
1171
  error_msg = str(llm_error)
1172
- if "Cannot connect" in error_msg or "Ollama" in error_msg:
1173
  fallback = (
1174
  f"I encountered an error while processing your request: {str(e)}\n\n"
1175
- f"Additionally, the AI service (Ollama) is unavailable: {error_msg}\n\n"
1176
  f"To fix:\n"
1177
- f"1. Install Ollama from https://ollama.ai\n"
1178
- f"2. Start: `ollama serve`\n"
1179
- f"3. Pull model: `ollama pull {os.getenv('OLLAMA_MODEL', 'llama3.1:latest')}`"
1180
  )
1181
  else:
1182
  fallback = f"I encountered an error while processing your request: {str(e)}. Additionally, the AI service is unavailable: {error_msg}"
@@ -1315,15 +1326,14 @@ Answer:"""
1315
  except Exception as e:
1316
  # If LLM fails, return a helpful error message
1317
  error_msg = str(e)
1318
- if "Cannot connect" in error_msg or "Ollama" in error_msg:
1319
  llm_out = (
1320
- f"I couldn't connect to the AI service (Ollama). "
1321
  f"Error: {error_msg}\n\n"
1322
  f"To fix this:\n"
1323
- f"1. Install Ollama from https://ollama.ai\n"
1324
- f"2. Start Ollama: `ollama serve`\n"
1325
- f"3. Pull the model: `ollama pull {os.getenv('OLLAMA_MODEL', 'llama3.1:latest')}`\n"
1326
- f"4. Or set OLLAMA_URL and OLLAMA_MODEL in your .env file"
1327
  )
1328
  else:
1329
  llm_out = f"I apologize, but I'm unable to process your request right now. The AI service is unavailable: {error_msg}"
@@ -1997,15 +2007,14 @@ Answer:"""
1997
  tool_traces.append({"tool": "llm", "error": str(e)})
1998
  error_msg = str(e)
1999
  # Provide helpful error message
2000
- if "Cannot connect" in error_msg or "Ollama" in error_msg:
2001
  fallback = (
2002
- f"I couldn't connect to the AI service (Ollama). "
2003
  f"Error: {error_msg}\n\n"
2004
  f"To fix this:\n"
2005
- f"1. Install Ollama from https://ollama.ai\n"
2006
- f"2. Start Ollama: `ollama serve`\n"
2007
- f"3. Pull the model: `ollama pull {os.getenv('OLLAMA_MODEL', 'llama3.1:latest')}`\n"
2008
- f"4. Or set OLLAMA_URL and OLLAMA_MODEL in your .env file"
2009
  )
2010
  else:
2011
  fallback = f"I encountered an error while synthesizing the response: {error_msg}"
 
40
 
41
  class AgentOrchestrator:
42
 
43
+ def __init__(self, rag_mcp_url: str, web_mcp_url: str, admin_mcp_url: str):
44
  self.mcp = MCPClient(rag_mcp_url, web_mcp_url, admin_mcp_url)
45
+ # Groq-only LLM client
46
+ self.llm = LLMClient(api_key=os.getenv("GROQ_API_KEY"), model=os.getenv("GROQ_MODEL"))
47
 
48
  # pass admin_mcp_url so detector can call back
49
  self.redflag = RedFlagDetector(
 
69
  return
70
 
71
  if self._analytics_disabled:
72
+ logger.info("Analytics: Disabled via ANALYTICS_DISABLED")
73
  else:
74
  store = self._get_analytics()
75
  if store is None:
76
+ # Only log if credentials might be missing (not if package is missing)
77
+ import os
78
+ if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"):
79
+ logger.warning("Analytics: Disabled (Supabase initialization failed)")
80
+ else:
81
+ logger.debug("Analytics: Disabled (Supabase not configured)")
82
  elif store.use_supabase:
83
+ logger.info("Analytics: Using Supabase backend")
84
  else:
85
+ logger.warning("Analytics: Using fallback backend")
86
 
87
  AgentOrchestrator._analytics_backend_logged = True
88
 
 
96
  try:
97
  self._analytics = AnalyticsStore()
98
  except RuntimeError as exc:
99
+ # Only log at warning level if credentials are configured (actual error)
100
+ # Otherwise log at debug level (expected when Supabase is not configured)
101
+ import os
102
+ if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"):
103
+ logger.warning("Analytics disabled: %s", str(exc).split('\n')[0]) # Only first line
104
+ else:
105
+ logger.debug("Analytics disabled: %s", str(exc).split('\n')[0])
106
  self._analytics_failed = True
107
  self._analytics = None
108
  except Exception as exc: # pragma: no cover - unexpected initialization failures
109
+ logger.debug("Analytics unexpected init failure: %s", exc)
110
  self._analytics_failed = True
111
  self._analytics = None
112
 
 
1181
  fallback = await self.llm.simple_call(req.message, temperature=req.temperature)
1182
  except Exception as llm_error:
1183
  error_msg = str(llm_error)
1184
+ if "Groq API key" in error_msg or "GROQ_API_KEY" in error_msg:
1185
  fallback = (
1186
  f"I encountered an error while processing your request: {str(e)}\n\n"
1187
+ f"Additionally, the AI service (Groq) is unavailable: {error_msg}\n\n"
1188
  f"To fix:\n"
1189
+ f"1. Get a free Groq API key from https://console.groq.com\n"
1190
+ f"2. Set GROQ_API_KEY in your .env file or environment variables"
 
1191
  )
1192
  else:
1193
  fallback = f"I encountered an error while processing your request: {str(e)}. Additionally, the AI service is unavailable: {error_msg}"
 
1326
  except Exception as e:
1327
  # If LLM fails, return a helpful error message
1328
  error_msg = str(e)
1329
+ if "Groq API key" in error_msg or "GROQ_API_KEY" in error_msg:
1330
  llm_out = (
1331
+ f"I couldn't connect to the AI service (Groq). "
1332
  f"Error: {error_msg}\n\n"
1333
  f"To fix this:\n"
1334
+ f"1. Get a free Groq API key from https://console.groq.com\n"
1335
+ f"2. Set GROQ_API_KEY in your .env file or environment variables\n"
1336
+ f"3. Optionally set GROQ_MODEL (default: llama-3.1-8b-instant)"
 
1337
  )
1338
  else:
1339
  llm_out = f"I apologize, but I'm unable to process your request right now. The AI service is unavailable: {error_msg}"
 
2007
  tool_traces.append({"tool": "llm", "error": str(e)})
2008
  error_msg = str(e)
2009
  # Provide helpful error message
2010
+ if "Groq API key" in error_msg or "GROQ_API_KEY" in error_msg:
2011
  fallback = (
2012
+ f"I couldn't connect to the AI service (Groq). "
2013
  f"Error: {error_msg}\n\n"
2014
  f"To fix this:\n"
2015
+ f"1. Get a free Groq API key from https://console.groq.com\n"
2016
+ f"2. Set GROQ_API_KEY in your .env file or environment variables\n"
2017
+ f"3. Optionally set GROQ_MODEL (default: llama-3.1-8b-instant)"
 
2018
  )
2019
  else:
2020
  fallback = f"I encountered an error while synthesizing the response: {error_msg}"
backend/api/services/document_ingestion.py CHANGED
@@ -324,7 +324,7 @@ async def process_ingestion(
324
  raise RuntimeError(
325
  f"Failed to send document to RAG MCP server: {str(e)}\n\n"
326
  f"Please check:\n"
327
- f"1. RAG_MCP_URL is set correctly (default: http://localhost:8001)\n"
328
  f"2. RAG MCP server is running\n"
329
  f"3. Database connection (POSTGRESQL_URL) is configured in the RAG server"
330
  ) from e
 
324
  raise RuntimeError(
325
  f"Failed to send document to RAG MCP server: {str(e)}\n\n"
326
  f"Please check:\n"
327
+ f"1. RAG_MCP_URL is set correctly (default: http://localhost:8900/rag)\n"
328
  f"2. RAG MCP server is running\n"
329
  f"3. Database connection (POSTGRESQL_URL) is configured in the RAG server"
330
  ) from e
backend/api/services/llm_client.py CHANGED
@@ -5,68 +5,65 @@ from typing import AsyncGenerator
5
 
6
  class LLMClient:
7
 
8
- def __init__(self, backend="ollama", url=None, api_key=None, model=None):
9
- self.backend = backend
10
- self.url = url or os.getenv("OLLAMA_URL", "http://localhost:11434")
11
  self.api_key = api_key or os.getenv("GROQ_API_KEY")
12
- # Default model based on backend
13
- if backend == "groq":
14
- self.model = model or os.getenv("GROQ_MODEL", "llama-3.1-70b-versatile")
15
- else:
16
- self.model = model or os.getenv("OLLAMA_MODEL", "llama3.1:latest")
17
  self.http = httpx.AsyncClient(timeout=30)
18
 
19
-
20
  async def simple_call(self, prompt: str, temperature: float = 0.0) -> str:
21
- if self.backend=="ollama":
22
- if not self.url or not self.model:
23
- raise RuntimeError(f"LLM not configured: url={self.url}, model={self.model}. Set OLLAMA_URL and OLLAMA_MODEL env vars.")
24
-
25
- try:
26
- # Ollama uses /api/generate endpoint
27
- r = await self.http.post(
28
- f"{self.url}/api/generate",
29
- json={
30
- "model": self.model,
31
- "prompt": prompt,
32
- "stream": False,
33
- "options": {"temperature": temperature}
34
- }
35
- )
36
- r.raise_for_status()
37
- response_data = r.json()
38
- return response_data.get("response", "")
39
- except httpx.HTTPStatusError as e:
40
- if e.response.status_code == 404:
41
- raise RuntimeError(
42
- f"Ollama endpoint not found. Is Ollama running at {self.url}? "
43
- f"Or does the model '{self.model}' exist? "
44
- f"Try: ollama pull {self.model}"
45
- )
46
- elif e.response.status_code == 400:
47
- error_detail = e.response.json().get("error", "Unknown error")
48
- raise RuntimeError(f"Ollama API error: {error_detail}")
49
- else:
50
- raise RuntimeError(f"Ollama API error: HTTP {e.response.status_code} - {e.response.text}")
51
- except httpx.ConnectError:
52
- raise RuntimeError(
53
- f"Cannot connect to Ollama at {self.url}. "
54
- f"Is Ollama running? Start it with: ollama serve"
55
- )
56
- except Exception as e:
57
- raise RuntimeError(f"LLM call failed: {str(e)}")
58
- elif self.backend == "groq":
59
- if not self.api_key:
60
- raise RuntimeError(
61
- "Groq API key not configured. Set GROQ_API_KEY environment variable. "
62
- "Get a free API key at https://console.groq.com"
63
- )
64
- if not self.model:
65
- raise RuntimeError("Groq model not configured. Set GROQ_MODEL environment variable.")
66
-
67
  try:
68
- # Groq uses OpenAI-compatible API
69
- r = await self.http.post(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  "https://api.groq.com/openai/v1/chat/completions",
71
  headers={
72
  "Authorization": f"Bearer {self.api_key}",
@@ -78,117 +75,32 @@ class LLMClient:
78
  {"role": "user", "content": prompt}
79
  ],
80
  "temperature": temperature,
81
- "stream": False
82
  }
83
- )
84
- r.raise_for_status()
85
- response_data = r.json()
86
- return response_data["choices"][0]["message"]["content"]
87
- except httpx.HTTPStatusError as e:
88
- error_detail = "Unknown error"
89
- try:
90
- error_json = e.response.json()
91
- error_detail = error_json.get("error", {}).get("message", str(error_json))
92
- except:
93
- error_detail = e.response.text
94
- raise RuntimeError(f"Groq API error: HTTP {e.response.status_code} - {error_detail}")
95
- except Exception as e:
96
- raise RuntimeError(f"Groq API call failed: {str(e)}")
97
- else:
98
- raise RuntimeError(f"Unsupported backend: {self.backend}. Supported backends: 'ollama', 'groq'")
99
-
100
- async def stream_call(self, prompt: str, temperature: float = 0.0) -> AsyncGenerator[str, None]:
101
- """Stream LLM response token by token."""
102
- if self.backend == "ollama":
103
- if not self.url or not self.model:
104
- raise RuntimeError(f"LLM not configured: url={self.url}, model={self.model}")
105
-
106
- try:
107
- async with httpx.AsyncClient(timeout=300.0) as client:
108
- async with client.stream(
109
- "POST",
110
- f"{self.url}/api/generate",
111
- json={
112
- "model": self.model,
113
- "prompt": prompt,
114
- "stream": True,
115
- "options": {"temperature": temperature}
116
- }
117
- ) as response:
118
- response.raise_for_status()
119
- async for line in response.aiter_lines():
120
- if line:
121
  try:
122
- data = json.loads(line)
123
- token = data.get("response", "")
 
124
  if token:
125
  yield token
126
- # Check if done
127
- if data.get("done", False):
128
- break
129
  except json.JSONDecodeError:
130
  continue
131
- # Yield empty string to keep connection alive if needed
132
- # This helps with buffering issues
133
- except httpx.ConnectError:
134
- raise RuntimeError(
135
- f"Cannot connect to Ollama at {self.url}. "
136
- f"Is Ollama running? Start it with: ollama serve"
137
- )
138
- except Exception as e:
139
- raise RuntimeError(f"LLM streaming failed: {str(e)}")
140
- elif self.backend == "groq":
141
- if not self.api_key:
142
- raise RuntimeError(
143
- "Groq API key not configured. Set GROQ_API_KEY environment variable. "
144
- "Get a free API key at https://console.groq.com"
145
- )
146
- if not self.model:
147
- raise RuntimeError("Groq model not configured. Set GROQ_MODEL environment variable.")
148
-
149
  try:
150
- async with httpx.AsyncClient(timeout=300.0) as client:
151
- async with client.stream(
152
- "POST",
153
- "https://api.groq.com/openai/v1/chat/completions",
154
- headers={
155
- "Authorization": f"Bearer {self.api_key}",
156
- "Content-Type": "application/json"
157
- },
158
- json={
159
- "model": self.model,
160
- "messages": [
161
- {"role": "user", "content": prompt}
162
- ],
163
- "temperature": temperature,
164
- "stream": True
165
- }
166
- ) as response:
167
- response.raise_for_status()
168
- async for line in response.aiter_lines():
169
- if line:
170
- # Groq uses Server-Sent Events format
171
- if line.startswith("data: "):
172
- data_str = line[6:] # Remove "data: " prefix
173
- if data_str.strip() == "[DONE]":
174
- break
175
- try:
176
- data = json.loads(data_str)
177
- delta = data.get("choices", [{}])[0].get("delta", {})
178
- token = delta.get("content", "")
179
- if token:
180
- yield token
181
- except json.JSONDecodeError:
182
- continue
183
- except httpx.HTTPStatusError as e:
184
- error_detail = "Unknown error"
185
- try:
186
- error_json = e.response.json()
187
- error_detail = error_json.get("error", {}).get("message", str(error_json))
188
- except:
189
- error_detail = e.response.text
190
- raise RuntimeError(f"Groq API streaming error: HTTP {e.response.status_code} - {error_detail}")
191
- except Exception as e:
192
- raise RuntimeError(f"Groq API streaming failed: {str(e)}")
193
- else:
194
- raise RuntimeError(f"Streaming not supported for backend: {self.backend}")
 
5
 
6
  class LLMClient:
7
 
8
+ def __init__(self, api_key=None, model=None):
 
 
9
  self.api_key = api_key or os.getenv("GROQ_API_KEY")
10
+ self.model = model or os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
 
 
 
 
11
  self.http = httpx.AsyncClient(timeout=30)
12
 
 
13
  async def simple_call(self, prompt: str, temperature: float = 0.0) -> str:
14
+ if not self.api_key:
15
+ raise RuntimeError(
16
+ "Groq API key not configured. Set GROQ_API_KEY environment variable. "
17
+ "Get a free API key at https://console.groq.com"
18
+ )
19
+ if not self.model:
20
+ raise RuntimeError("Groq model not configured. Set GROQ_MODEL environment variable.")
21
+
22
+ try:
23
+ # Groq uses OpenAI-compatible API
24
+ r = await self.http.post(
25
+ "https://api.groq.com/openai/v1/chat/completions",
26
+ headers={
27
+ "Authorization": f"Bearer {self.api_key}",
28
+ "Content-Type": "application/json"
29
+ },
30
+ json={
31
+ "model": self.model,
32
+ "messages": [
33
+ {"role": "user", "content": prompt}
34
+ ],
35
+ "temperature": temperature,
36
+ "stream": False
37
+ }
38
+ )
39
+ r.raise_for_status()
40
+ response_data = r.json()
41
+ return response_data["choices"][0]["message"]["content"]
42
+ except httpx.HTTPStatusError as e:
43
+ error_detail = "Unknown error"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  try:
45
+ error_json = e.response.json()
46
+ error_detail = error_json.get("error", {}).get("message", str(error_json))
47
+ except:
48
+ error_detail = e.response.text
49
+ raise RuntimeError(f"Groq API error: HTTP {e.response.status_code} - {error_detail}")
50
+ except Exception as e:
51
+ raise RuntimeError(f"Groq API call failed: {str(e)}")
52
+
53
+ async def stream_call(self, prompt: str, temperature: float = 0.0) -> AsyncGenerator[str, None]:
54
+ """Stream LLM response token by token."""
55
+ if not self.api_key:
56
+ raise RuntimeError(
57
+ "Groq API key not configured. Set GROQ_API_KEY environment variable. "
58
+ "Get a free API key at https://console.groq.com"
59
+ )
60
+ if not self.model:
61
+ raise RuntimeError("Groq model not configured. Set GROQ_MODEL environment variable.")
62
+
63
+ try:
64
+ async with httpx.AsyncClient(timeout=300.0) as client:
65
+ async with client.stream(
66
+ "POST",
67
  "https://api.groq.com/openai/v1/chat/completions",
68
  headers={
69
  "Authorization": f"Bearer {self.api_key}",
 
75
  {"role": "user", "content": prompt}
76
  ],
77
  "temperature": temperature,
78
+ "stream": True
79
  }
80
+ ) as response:
81
+ response.raise_for_status()
82
+ async for line in response.aiter_lines():
83
+ if line:
84
+ # Groq uses Server-Sent Events format
85
+ if line.startswith("data: "):
86
+ data_str = line[6:] # Remove "data: " prefix
87
+ if data_str.strip() == "[DONE]":
88
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  try:
90
+ data = json.loads(data_str)
91
+ delta = data.get("choices", [{}])[0].get("delta", {})
92
+ token = delta.get("content", "")
93
  if token:
94
  yield token
 
 
 
95
  except json.JSONDecodeError:
96
  continue
97
+ except httpx.HTTPStatusError as e:
98
+ error_detail = "Unknown error"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  try:
100
+ error_json = e.response.json()
101
+ error_detail = error_json.get("error", {}).get("message", str(error_json))
102
+ except:
103
+ error_detail = e.response.text
104
+ raise RuntimeError(f"Groq API streaming error: HTTP {e.response.status_code} - {error_detail}")
105
+ except Exception as e:
106
+ raise RuntimeError(f"Groq API streaming failed: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/services/metadata_extractor.py CHANGED
@@ -24,10 +24,8 @@ class MetadataExtractor:
24
 
25
  def __init__(self, llm_client: Optional[LLMClient] = None):
26
  self.llm = llm_client or LLMClient(
27
- backend=os.getenv("LLM_BACKEND", "ollama"),
28
- url=os.getenv("OLLAMA_URL"),
29
  api_key=os.getenv("GROQ_API_KEY"),
30
- model=os.getenv("OLLAMA_MODEL", "llama3.1:latest")
31
  )
32
 
33
  async def extract_metadata(
 
24
 
25
  def __init__(self, llm_client: Optional[LLMClient] = None):
26
  self.llm = llm_client or LLMClient(
 
 
27
  api_key=os.getenv("GROQ_API_KEY"),
28
+ model=os.getenv("GROQ_MODEL")
29
  )
30
 
31
  async def extract_metadata(
backend/api/services/rule_enhancer.py CHANGED
@@ -16,10 +16,8 @@ class RuleEnhancer:
16
 
17
  def __init__(self, llm_client: Optional[LLMClient] = None):
18
  self.llm = llm_client or LLMClient(
19
- backend=os.getenv("LLM_BACKEND", "ollama"),
20
- url=os.getenv("OLLAMA_URL"),
21
  api_key=os.getenv("GROQ_API_KEY"),
22
- model=os.getenv("OLLAMA_MODEL", "llama3.1:latest")
23
  )
24
 
25
  async def enhance_rule(
 
16
 
17
  def __init__(self, llm_client: Optional[LLMClient] = None):
18
  self.llm = llm_client or LLMClient(
 
 
19
  api_key=os.getenv("GROQ_API_KEY"),
20
+ model=os.getenv("GROQ_MODEL")
21
  )
22
 
23
  async def enhance_rule(
backend/api/storage/analytics_store.py CHANGED
@@ -21,9 +21,13 @@ try:
21
  from supabase import Client, create_client
22
 
23
  SUPABASE_AVAILABLE = True
24
- except ImportError:
 
25
  Client = None # type: ignore
26
  SUPABASE_AVAILABLE = False
 
 
 
27
 
28
  logger = logging.getLogger(__name__)
29
 
@@ -63,15 +67,12 @@ class AnalyticsStore:
63
 
64
  if not SUPABASE_AVAILABLE:
65
  raise RuntimeError(
66
- "Supabase package not installed. Install with: pip install supabase\n"
67
- "AnalyticsStore requires Supabase - SQLite fallback has been removed."
68
  )
69
 
70
  if not supabase_url or not supabase_key:
71
  raise RuntimeError(
72
- "Supabase credentials are required!\n"
73
- "Set SUPABASE_URL and SUPABASE_SERVICE_KEY in your .env file.\n"
74
- "AnalyticsStore requires Supabase - SQLite fallback has been removed."
75
  )
76
 
77
  self.use_supabase = True # Always True - no fallback
@@ -110,9 +111,8 @@ class AnalyticsStore:
110
  except Exception as exc:
111
  logger.error(f"❌ Failed to initialize Supabase client for analytics: {exc}")
112
  raise RuntimeError(
113
- f"Failed to initialize Supabase client: {exc}\n"
114
- "Make sure SUPABASE_URL and SUPABASE_SERVICE_KEY are correct.\n"
115
- "AnalyticsStore requires Supabase - SQLite fallback has been removed."
116
  ) from exc
117
 
118
  def _quick_table_check(self):
@@ -192,8 +192,7 @@ class AnalyticsStore:
192
  )
193
  # Re-raise - no SQLite fallback
194
  raise RuntimeError(
195
- f"Failed to insert into Supabase table '{table}': {error_msg}\n"
196
- "AnalyticsStore requires Supabase - SQLite fallback has been removed."
197
  ) from exc
198
 
199
  def _supabase_simple_select(
 
21
  from supabase import Client, create_client
22
 
23
  SUPABASE_AVAILABLE = True
24
+ except (ImportError, Exception) as e:
25
+ # Handle both ImportError and other exceptions (e.g., websockets.asyncio issues)
26
  Client = None # type: ignore
27
  SUPABASE_AVAILABLE = False
28
+ # Only log at debug level to avoid noise - this is expected in some deployments
29
+ import logging
30
+ logging.getLogger(__name__).debug(f"Supabase import failed: {e}")
31
 
32
  logger = logging.getLogger(__name__)
33
 
 
67
 
68
  if not SUPABASE_AVAILABLE:
69
  raise RuntimeError(
70
+ "Supabase package not installed. Install with: pip install supabase"
 
71
  )
72
 
73
  if not supabase_url or not supabase_key:
74
  raise RuntimeError(
75
+ "Supabase credentials required. Set SUPABASE_URL and SUPABASE_SERVICE_KEY."
 
 
76
  )
77
 
78
  self.use_supabase = True # Always True - no fallback
 
111
  except Exception as exc:
112
  logger.error(f"❌ Failed to initialize Supabase client for analytics: {exc}")
113
  raise RuntimeError(
114
+ f"Failed to initialize Supabase client: {exc}. "
115
+ "Verify SUPABASE_URL and SUPABASE_SERVICE_KEY are correct."
 
116
  ) from exc
117
 
118
  def _quick_table_check(self):
 
192
  )
193
  # Re-raise - no SQLite fallback
194
  raise RuntimeError(
195
+ f"Failed to insert into Supabase table '{table}': {error_msg}"
 
196
  ) from exc
197
 
198
  def _supabase_simple_select(
backend/mcp_server/common/logging.py CHANGED
@@ -51,7 +51,13 @@ def _get_analytics_store() -> Optional["AnalyticsStore"]:
51
  try:
52
  _analytics_store = AnalyticsStore()
53
  except RuntimeError as exc:
54
- logger.warning("Analytics disabled: %s", exc)
 
 
 
 
 
 
55
  _analytics_failed = True
56
  _analytics_store = None
57
  except Exception as exc: # pragma: no cover - unexpected failures
 
51
  try:
52
  _analytics_store = AnalyticsStore()
53
  except RuntimeError as exc:
54
+ # Only log at warning level if credentials are configured (actual error)
55
+ # Otherwise log at debug level (expected when Supabase is not configured)
56
+ import os
57
+ if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"):
58
+ logger.warning("Analytics disabled: %s", str(exc).split('\n')[0]) # Only first line
59
+ else:
60
+ logger.debug("Analytics disabled: %s", str(exc).split('\n')[0])
61
  _analytics_failed = True
62
  _analytics_store = None
63
  except Exception as exc: # pragma: no cover - unexpected failures
docker-commands.ps1 ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # IntegraChat Docker Helper Commands for PowerShell
2
+
3
+ # Function to start/restart the container
4
+ function Start-IntegraChat {
5
+ Write-Host "Starting IntegraChat container..." -ForegroundColor Green
6
+
7
+ # Check if container exists
8
+ $exists = docker ps -a --filter "name=integrachat" --format "{{.Names}}"
9
+
10
+ if ($exists -eq "integrachat") {
11
+ Write-Host "Container exists. Stopping and removing..." -ForegroundColor Yellow
12
+ docker stop integrachat 2>$null
13
+ docker rm integrachat 2>$null
14
+ }
15
+
16
+ # Run the container
17
+ docker run -d --name integrachat `
18
+ -p 7860:7860 `
19
+ -p 8000:8000 `
20
+ -p 8900:8900 `
21
+ -e DOCKER_CONTAINER=1 `
22
+ integrachat:latest
23
+
24
+ if ($LASTEXITCODE -eq 0) {
25
+ Write-Host "`n✅ Container started!" -ForegroundColor Green
26
+ Write-Host "`nAccess services:" -ForegroundColor Cyan
27
+ Write-Host " • Gradio UI: http://localhost:7860"
28
+ Write-Host " • FastAPI: http://localhost:8000"
29
+ Write-Host " • MCP Server: http://localhost:8900"
30
+ Write-Host "`nView logs: docker logs -f integrachat" -ForegroundColor Yellow
31
+ }
32
+ }
33
+
34
+ # Function to stop the container
35
+ function Stop-IntegraChat {
36
+ Write-Host "Stopping IntegraChat container..." -ForegroundColor Yellow
37
+ docker stop integrachat
38
+ }
39
+
40
+ # Function to view logs
41
+ function Show-IntegraChatLogs {
42
+ docker logs -f integrachat
43
+ }
44
+
45
+ # Function to rebuild
46
+ function Rebuild-IntegraChat {
47
+ Write-Host "Rebuilding IntegraChat..." -ForegroundColor Green
48
+ docker stop integrachat 2>$null
49
+ docker rm integrachat 2>$null
50
+ docker build -t integrachat:latest .
51
+ Start-IntegraChat
52
+ }
53
+
54
+ # Export functions
55
+ Export-ModuleMember -Function Start-IntegraChat, Stop-IntegraChat, Show-IntegraChatLogs, Rebuild-IntegraChat
56
+
docker-compose.yml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ integrachat:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ container_name: integrachat
7
+ ports:
8
+ - "7860:7860" # Gradio UI
9
+ - "8000:8000" # FastAPI
10
+ - "8900:8900" # MCP Server
11
+ environment:
12
+ - API_PORT=8000
13
+ - MCP_PORT=8900
14
+ - GRADIO_PORT=7860
15
+ - DOCKER_CONTAINER=1
16
+ # Add your environment variables here or use env_file
17
+ # - SUPABASE_URL=${SUPABASE_URL}
18
+ # - SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY}
19
+ # - GROQ_API_KEY=${GROQ_API_KEY}
20
+ # - OLLAMA_BASE_URL=${OLLAMA_BASE_URL}
21
+ env_file:
22
+ - .env # Optional: load from .env file if it exists
23
+ volumes:
24
+ # Optional: mount logs directory for persistence
25
+ - ./logs:/app/logs
26
+ restart: unless-stopped
27
+ healthcheck:
28
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
29
+ interval: 30s
30
+ timeout: 10s
31
+ retries: 3
32
+ start_period: 60s
33
+
docker-entrypoint.sh CHANGED
@@ -19,8 +19,11 @@ log "API_PORT=${API_PORT}, MCP_PORT=${MCP_PORT}, GRADIO_PORT=${GRADIO_PORT}"
19
 
20
  cleanup() {
21
  log "Received termination signal. Stopping services..."
22
- # Kill child processes
23
- kill "${MCP_PID}" "${API_PID}" "${GRADIO_PID}" "${TAIL_PID}" 2>/dev/null || true
 
 
 
24
  wait || true
25
  log "All services stopped. Exiting."
26
  }
 
19
 
20
  cleanup() {
21
  log "Received termination signal. Stopping services..."
22
+ # Kill child processes (only if they exist)
23
+ [ -n "${MCP_PID:-}" ] && kill "${MCP_PID}" 2>/dev/null || true
24
+ [ -n "${API_PID:-}" ] && kill "${API_PID}" 2>/dev/null || true
25
+ [ -n "${GRADIO_PID:-}" ] && kill "${GRADIO_PID}" 2>/dev/null || true
26
+ [ -n "${TAIL_PID:-}" ] && kill "${TAIL_PID}" 2>/dev/null || true
27
  wait || true
28
  log "All services stopped. Exiting."
29
  }
env.example CHANGED
@@ -11,28 +11,19 @@ SUPABASE_SERVICE_KEY=your_service_role_key_here
11
  POSTGRESQL_URL=postgresql://user:password@host:port/database
12
 
13
  # =============================================================
14
- # LLM CONFIGURATION
15
  # =============================================================
16
- # Backend selection: "ollama" (local) or "groq" (cloud API)
17
- # For Hugging Face Spaces, use "groq"
18
- LLM_BACKEND=groq
19
-
20
- # Option 1: Using Groq API (recommended for Hugging Face Spaces)
21
  # Get free API key at https://console.groq.com
22
  GROQ_API_KEY=your_groq_api_key_here
23
- GROQ_MODEL=llama-3.1-70b-versatile
24
-
25
- # Option 2: Using local Ollama (for local development)
26
- # OLLAMA_URL=http://localhost:11434
27
- # OLLAMA_MODEL=llama3.1:latest
28
 
29
  # =============================================================
30
  # MCP SERVER CONFIG
31
  # =============================================================
32
- # Legacy FastAPI endpoints (remove once all callers use the unified MCP server)
33
- RAG_MCP_URL=http://localhost:8001
34
- WEB_MCP_URL=http://localhost:8002
35
- ADMIN_MCP_URL=http://localhost:8003
36
 
37
  # Unified MCP server identifier (namespaced tools)
38
  MCP_SERVER_ID=integrachat
 
11
  POSTGRESQL_URL=postgresql://user:password@host:port/database
12
 
13
  # =============================================================
14
+ # LLM CONFIGURATION (Groq Only)
15
  # =============================================================
 
 
 
 
 
16
  # Get free API key at https://console.groq.com
17
  GROQ_API_KEY=your_groq_api_key_here
18
+ GROQ_MODEL=llama-3.1-8b-instant
 
 
 
 
19
 
20
  # =============================================================
21
  # MCP SERVER CONFIG
22
  # =============================================================
23
+ # Unified MCP server endpoints (running on port 8900)
24
+ RAG_MCP_URL=http://localhost:8900/rag
25
+ WEB_MCP_URL=http://localhost:8900/web
26
+ ADMIN_MCP_URL=http://localhost:8900/admin
27
 
28
  # Unified MCP server identifier (namespaced tools)
29
  MCP_SERVER_ID=integrachat
run-docker.ps1 ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PowerShell script to run IntegraChat Docker container
2
+
3
+ # Stop and remove existing container if it exists
4
+ Write-Host "Checking for existing container..." -ForegroundColor Yellow
5
+ $existing = docker ps -a --filter "name=integrachat" --format "{{.Names}}" 2>&1
6
+ if ($existing -eq "integrachat") {
7
+ Write-Host "Removing existing container (force)..." -ForegroundColor Yellow
8
+ # Use -f to force remove (stops and removes in one command)
9
+ docker rm -f integrachat 2>&1 | Out-Null
10
+ Start-Sleep -Seconds 1
11
+ }
12
+
13
+ # Build the image
14
+ Write-Host "Building Docker image (this may take a few minutes)..." -ForegroundColor Green
15
+ Write-Host "Progress will be shown below..." -ForegroundColor Gray
16
+ docker build -t integrachat:latest .
17
+
18
+ if ($LASTEXITCODE -ne 0) {
19
+ Write-Host "Build failed! Check the error messages above." -ForegroundColor Red
20
+ exit 1
21
+ }
22
+ Write-Host "Build completed successfully!" -ForegroundColor Green
23
+
24
+ # Check if .env file exists
25
+ if (-not (Test-Path .env)) {
26
+ Write-Host "Warning: .env file not found. Creating from env.example..." -ForegroundColor Yellow
27
+ if (Test-Path env.example) {
28
+ Copy-Item env.example .env
29
+ Write-Host "Created .env file. Please update it with your configuration." -ForegroundColor Yellow
30
+ } else {
31
+ Write-Host "Error: env.example not found. Cannot create .env file." -ForegroundColor Red
32
+ exit 1
33
+ }
34
+ }
35
+
36
+ # Run the container
37
+ Write-Host "Starting container..." -ForegroundColor Green
38
+ docker run -d --name integrachat `
39
+ -p 7860:7860 `
40
+ -p 8000:8000 `
41
+ -p 8900:8900 `
42
+ --env-file .env `
43
+ -e DOCKER_CONTAINER=1 `
44
+ integrachat:latest
45
+
46
+ if ($LASTEXITCODE -eq 0) {
47
+ Write-Host "Container started successfully!" -ForegroundColor Green
48
+ Write-Host ""
49
+ Write-Host "Access services:" -ForegroundColor Cyan
50
+ Write-Host " - Gradio UI: http://localhost:7860" -ForegroundColor White
51
+ Write-Host " - FastAPI: http://localhost:8000" -ForegroundColor White
52
+ Write-Host " - MCP Server: http://localhost:8900" -ForegroundColor White
53
+ Write-Host ""
54
+ Write-Host "View logs: docker logs -f integrachat" -ForegroundColor Yellow
55
+ } else {
56
+ Write-Host "Failed to start container!" -ForegroundColor Red
57
+ exit 1
58
+ }
59
+