dboa9 Cursor commited on
Commit
9c0f869
·
1 Parent(s): f99e52c

Fix: Multi-service Dockerfile + missing API endpoints

Browse files

- Dockerfile: python:3.11-slim base with Ollama installed via curl
(was: ollama/ollama base with no Python - FastAPI never started)
- app.py v3.0: Added /api/generate (for cloud_llm_adapter.py)
and /tools/analyze_report (for trigger_cloud.py)
- start.sh: Dual-service startup (Ollama bg + FastAPI fg)
- Model pull moved to runtime to avoid build timeout
- Added python-multipart for file upload support

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (4) hide show
  1. Dockerfile +29 -6
  2. app.py +219 -34
  3. requirements.txt +2 -1
  4. start.sh +46 -6
Dockerfile CHANGED
@@ -1,8 +1,31 @@
1
- FROM ollama/ollama:latest
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  ENV OLLAMA_HOST=0.0.0.0
3
- RUN ollama serve & \
4
- sleep 5 && \
5
- ollama pull qwen2.5:7b && \
6
- killall ollama
 
 
 
 
 
 
 
 
7
  EXPOSE 7860
8
- ENTRYPOINT ["ollama", "serve"]
 
 
 
1
+ # Moltbot Hybrid Engine - Multi-service Dockerfile
2
+ # Runs: FastAPI (port 7860) + Ollama (port 11434, background)
3
+ # Build: 2026-02-06
4
+ FROM python:3.11-slim
5
+
6
+ # Install system dependencies + Ollama
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ curl \
9
+ procps \
10
+ && curl -fsSL https://ollama.com/install.sh | sh \
11
+ && apt-get clean \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Set environment
15
  ENV OLLAMA_HOST=0.0.0.0
16
+ ENV OLLAMA_MODELS=/app/models
17
+ WORKDIR /app
18
+
19
+ # Copy and install Python dependencies first (Docker layer caching)
20
+ COPY requirements.txt .
21
+ RUN pip install --no-cache-dir -r requirements.txt
22
+
23
+ # Copy application files
24
+ COPY app.py start.sh ./
25
+ RUN chmod +x start.sh
26
+
27
+ # Expose HF Spaces port
28
  EXPOSE 7860
29
+
30
+ # Start both services via start.sh
31
+ CMD ["bash", "start.sh"]
app.py CHANGED
@@ -1,33 +1,53 @@
1
  """
2
- Moltbot Hybrid Engine - SAFE DEPLOYMENT v2.0.0
3
- Brain only - NO file access, NO local filesystem access
4
- Runs on Hugging Face Spaces in isolated container
5
- Build: 2026-01-31
 
 
 
 
 
 
 
 
 
 
6
  """
7
  import os
8
  import re
9
  import json
10
- from fastapi import FastAPI, HTTPException, Header
 
 
11
  from pydantic import BaseModel
12
  from typing import List, Optional, Dict, Any
13
 
 
 
 
14
  # Initialize App
15
  app = FastAPI(
16
  title="Moltbot Hybrid Engine",
17
- description="Safe AI agent for file matching - brain only, no file access",
18
- version="2.0.0"
19
  )
20
 
21
  # API Key for authentication
22
  API_KEY = os.environ.get("MOLTBOT_API_KEY", "default_insecure_key")
23
  if API_KEY == "default_insecure_key":
24
- print("WARNING: MOLTBOT_API_KEY not set. Using insecure default.")
25
 
26
 
27
  # ============================================================
28
  # DATA MODELS
29
  # ============================================================
30
 
 
 
 
 
 
31
  class FileSearchRequest(BaseModel):
32
  missing_filename: str
33
  available_files: List[str]
@@ -50,10 +70,11 @@ class AnalysisResponse(BaseModel):
50
 
51
 
52
  # ============================================================
53
- # HELPER FUNCTIONS (Pure logic, no file access)
54
  # ============================================================
55
 
56
  def tokenize(text: str) -> set:
 
57
  clean = re.sub(r'[_\-\.\(\)\[\]]', ' ', text.lower())
58
  tokens = set(clean.split())
59
  junk = {'pdf', 'mp4', 'jpg', 'jpeg', 'png', 'gif', 'doc', 'docx',
@@ -61,16 +82,18 @@ def tokenize(text: str) -> set:
61
  return tokens - junk
62
 
63
  def calculate_match_score(wanted: set, found: set) -> float:
 
64
  if not wanted:
65
  return 0.0
66
  common = wanted.intersection(found)
67
  return len(common) / len(wanted)
68
 
69
  def find_best_matches(missing_filename: str, available_files: List[str], max_results: int = 5) -> List[Dict[str, Any]]:
 
70
  wanted_tokens = tokenize(missing_filename)
71
  if not wanted_tokens:
72
  return []
73
-
74
  matches = []
75
  for filename in available_files:
76
  if filename == missing_filename:
@@ -83,10 +106,58 @@ def find_best_matches(missing_filename: str, available_files: List[str], max_res
83
  score = calculate_match_score(wanted_tokens, found_tokens)
84
  if score >= 0.5:
85
  matches.append({"filename": filename, "score": round(score, 3), "match_type": "token_match"})
86
-
87
  matches.sort(key=lambda x: x["score"], reverse=True)
88
  return matches[:max_results]
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  # ============================================================
92
  # API ENDPOINTS
@@ -94,85 +165,199 @@ def find_best_matches(missing_filename: str, available_files: List[str], max_res
94
 
95
  @app.get("/")
96
  def health_check():
 
 
97
  return {
98
  "status": "running",
99
  "service": "Moltbot Hybrid Engine",
100
- "version": "2.0.0",
101
- "mode": "SAFE - Brain only, no file access"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
 
104
  @app.get("/security")
105
  def security_info():
 
106
  return {
107
  "file_access": False,
108
  "network_access": "API only",
109
  "isolation": "Hugging Face container",
110
- "cannot_do": ["Read files", "Write files", "Delete files", "Access filesystem", "Execute commands"]
 
111
  }
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  @app.post("/api/search", response_model=FileSearchResponse)
114
  async def search_file(request: FileSearchRequest, x_api_key: str = Header(None)):
 
115
  if not x_api_key or x_api_key != API_KEY:
116
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
117
-
118
  if len(request.missing_filename) > 200:
119
  return FileSearchResponse(
120
  status="error", missing_filename=request.missing_filename[:50] + "...",
121
  suggestions=[], confidence=0.0,
122
  reasoning="Filename too long - likely concatenated filenames"
123
  )
124
-
125
  matches = find_best_matches(request.missing_filename, request.available_files)
126
  confidence = matches[0]["score"] if matches else 0.0
127
-
128
  if not matches:
129
  reasoning = f"No matches found in {len(request.available_files)} files"
130
  elif matches[0]["match_type"] == "exact":
131
  reasoning = f"Exact match: {matches[0]['filename']}"
132
  else:
133
  reasoning = f"Token match with {int(confidence * 100)}% similarity"
134
-
135
  return FileSearchResponse(
136
  status="success", missing_filename=request.missing_filename,
137
  suggestions=matches, confidence=confidence, reasoning=reasoning
138
  )
139
 
 
 
 
140
  @app.post("/api/analyze", response_model=AnalysisResponse)
141
- async def analyze_report(request: AnalysisRequest, x_api_key: str = Header(None)):
 
142
  if not x_api_key or x_api_key != API_KEY:
143
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
144
-
145
- data = request.report_data
146
- suggestions = []
147
- missing_files = data.get("missing_total", 0)
148
- structure_issues = data.get("page_structure_analysis", {}).get("bundles_with_structure_issues", 0)
149
-
150
- if missing_files > 0:
151
- suggestions.append(f"{missing_files} files missing - check paths")
152
- if structure_issues > 0:
153
- suggestions.append(f"{structure_issues} bundles have structure problems")
154
-
155
  return AnalysisResponse(
156
- status="success", critical_issues=missing_files + structure_issues, suggestions=suggestions
 
 
157
  )
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  @app.post("/api/extract_date")
160
  async def extract_date(filename: str, x_api_key: str = Header(None)):
 
161
  if not x_api_key or x_api_key != API_KEY:
162
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
163
-
164
  patterns = [
165
  (r'(\d{4})-(\d{2})-(\d{2})', 'ISO'),
166
  (r'(\d{4})_(\d{2})_(\d{2})', 'underscore'),
 
167
  (r'(\d{4})(\d{2})(\d{2})', 'compact'),
168
  ]
169
  for pattern, fmt in patterns:
170
  match = re.search(pattern, filename)
171
  if match:
172
- year, month, day = match.groups()
173
- return {"status": "found", "date": f"{year}-{int(month):02d}-{int(day):02d}"}
 
 
 
 
 
 
 
 
174
  return {"status": "not_found", "date": None}
175
 
 
176
  if __name__ == "__main__":
177
  import uvicorn
178
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
+ Moltbot Hybrid Engine - Production v3.0.0
3
+ Multi-service: FastAPI endpoints + Ollama LLM integration
4
+ Runs on Hugging Face Spaces
5
+ Build: 2026-02-06
6
+
7
+ Endpoints:
8
+ GET / - Health check
9
+ GET /health - Detailed health status
10
+ GET /security - Security posture info
11
+ POST /api/generate - LLM text generation via Ollama (called by cloud_llm_adapter.py)
12
+ POST /api/search - Fuzzy file matching (called by file resolution)
13
+ POST /api/analyze - Report analysis (JSON body)
14
+ POST /api/extract_date - Date extraction from filenames
15
+ POST /tools/analyze_report - Report analysis via file upload (called by trigger_cloud.py)
16
  """
17
  import os
18
  import re
19
  import json
20
+ import subprocess
21
+ import logging
22
+ from fastapi import FastAPI, HTTPException, Header, UploadFile, File
23
  from pydantic import BaseModel
24
  from typing import List, Optional, Dict, Any
25
 
26
+ logging.basicConfig(level=logging.INFO)
27
+ logger = logging.getLogger("moltbot-engine")
28
+
29
  # Initialize App
30
  app = FastAPI(
31
  title="Moltbot Hybrid Engine",
32
+ description="AI agent for legal document processing - LLM + file matching + analysis",
33
+ version="3.0.0"
34
  )
35
 
36
  # API Key for authentication
37
  API_KEY = os.environ.get("MOLTBOT_API_KEY", "default_insecure_key")
38
  if API_KEY == "default_insecure_key":
39
+ logger.warning("MOLTBOT_API_KEY not set. Using insecure default.")
40
 
41
 
42
  # ============================================================
43
  # DATA MODELS
44
  # ============================================================
45
 
46
+ class GenerateRequest(BaseModel):
47
+ model: str = "qwen2.5:7b"
48
+ prompt: str
49
+ stream: bool = False
50
+
51
  class FileSearchRequest(BaseModel):
52
  missing_filename: str
53
  available_files: List[str]
 
70
 
71
 
72
  # ============================================================
73
+ # HELPER FUNCTIONS
74
  # ============================================================
75
 
76
  def tokenize(text: str) -> set:
77
+ """Tokenize a filename for fuzzy matching."""
78
  clean = re.sub(r'[_\-\.\(\)\[\]]', ' ', text.lower())
79
  tokens = set(clean.split())
80
  junk = {'pdf', 'mp4', 'jpg', 'jpeg', 'png', 'gif', 'doc', 'docx',
 
82
  return tokens - junk
83
 
84
  def calculate_match_score(wanted: set, found: set) -> float:
85
+ """Calculate token overlap score between two sets."""
86
  if not wanted:
87
  return 0.0
88
  common = wanted.intersection(found)
89
  return len(common) / len(wanted)
90
 
91
  def find_best_matches(missing_filename: str, available_files: List[str], max_results: int = 5) -> List[Dict[str, Any]]:
92
+ """Find best fuzzy matches for a missing filename."""
93
  wanted_tokens = tokenize(missing_filename)
94
  if not wanted_tokens:
95
  return []
96
+
97
  matches = []
98
  for filename in available_files:
99
  if filename == missing_filename:
 
106
  score = calculate_match_score(wanted_tokens, found_tokens)
107
  if score >= 0.5:
108
  matches.append({"filename": filename, "score": round(score, 3), "match_type": "token_match"})
109
+
110
  matches.sort(key=lambda x: x["score"], reverse=True)
111
  return matches[:max_results]
112
 
113
+ def check_ollama_status() -> dict:
114
+ """Check if Ollama is running and responsive."""
115
+ try:
116
+ result = subprocess.run(
117
+ ["ollama", "list"],
118
+ capture_output=True, text=True, timeout=10
119
+ )
120
+ if result.returncode == 0:
121
+ models = [line.split()[0] for line in result.stdout.strip().split('\n')[1:] if line.strip()]
122
+ return {"running": True, "models": models}
123
+ return {"running": False, "error": result.stderr.strip()}
124
+ except FileNotFoundError:
125
+ return {"running": False, "error": "ollama binary not found"}
126
+ except subprocess.TimeoutExpired:
127
+ return {"running": False, "error": "ollama list timed out"}
128
+ except Exception as e:
129
+ return {"running": False, "error": str(e)}
130
+
131
+ def analyze_report_data(data: dict) -> dict:
132
+ """Analyze a verification report and return findings."""
133
+ suggestions = []
134
+ missing_files = data.get("missing_total", 0)
135
+ structure_issues = data.get("page_structure_analysis", {}).get("bundles_with_structure_issues", 0)
136
+ blank_pages = data.get("blank_placeholder_pages", 0)
137
+
138
+ if missing_files > 0:
139
+ suggestions.append(f"{missing_files} files missing - check paths in proven_paths_index.json")
140
+ if structure_issues > 0:
141
+ suggestions.append(f"{structure_issues} bundles have page structure problems - check embedding logic")
142
+ if blank_pages > 0:
143
+ suggestions.append(f"{blank_pages} blank placeholder pages found - files listed in TOC but not embedded")
144
+
145
+ # Check per-bundle issues
146
+ bundles = data.get("bundles", {})
147
+ for bundle_name, bundle_data in bundles.items():
148
+ if isinstance(bundle_data, dict):
149
+ bundle_missing = bundle_data.get("missing_count", 0)
150
+ if bundle_missing > 0:
151
+ suggestions.append(f"Bundle {bundle_name}: {bundle_missing} files missing")
152
+
153
+ critical_count = missing_files + structure_issues + blank_pages
154
+ return {
155
+ "status": "success",
156
+ "critical_issues": critical_count,
157
+ "suggestions": suggestions if suggestions else ["No critical issues found"],
158
+ "summary": f"Analyzed report: {critical_count} critical issues across {len(bundles)} bundles"
159
+ }
160
+
161
 
162
  # ============================================================
163
  # API ENDPOINTS
 
165
 
166
  @app.get("/")
167
  def health_check():
168
+ """Basic health check."""
169
+ ollama = check_ollama_status()
170
  return {
171
  "status": "running",
172
  "service": "Moltbot Hybrid Engine",
173
+ "version": "3.0.0",
174
+ "ollama": ollama
175
+ }
176
+
177
+ @app.get("/health")
178
+ def detailed_health():
179
+ """Detailed health check with Ollama status."""
180
+ ollama = check_ollama_status()
181
+ return {
182
+ "status": "healthy",
183
+ "service": "moltbot-hybrid-engine",
184
+ "version": "3.0.0",
185
+ "ollama_running": ollama.get("running", False),
186
+ "ollama_models": ollama.get("models", []),
187
+ "endpoints": ["/", "/health", "/api/generate", "/api/search",
188
+ "/api/analyze", "/api/extract_date", "/tools/analyze_report"]
189
  }
190
 
191
  @app.get("/security")
192
  def security_info():
193
+ """Report security posture."""
194
  return {
195
  "file_access": False,
196
  "network_access": "API only",
197
  "isolation": "Hugging Face container",
198
+ "cannot_do": ["Read local files", "Write local files", "Delete files",
199
+ "Access host filesystem", "Execute arbitrary commands"]
200
  }
201
 
202
+
203
+ # --- LLM Generation (called by cloud_llm_adapter.py) ---
204
+
205
+ @app.post("/api/generate")
206
+ async def generate(request: GenerateRequest, x_api_key: str = Header(None)):
207
+ """Generate text using Ollama. Called by cloud_llm_adapter.py."""
208
+ if not x_api_key or x_api_key != API_KEY:
209
+ raise HTTPException(status_code=401, detail="Invalid or missing API Key")
210
+
211
+ logger.info(f"[GENERATE] model={request.model}, prompt_len={len(request.prompt)}")
212
+
213
+ # Check Ollama availability first
214
+ ollama_status = check_ollama_status()
215
+ if not ollama_status.get("running"):
216
+ raise HTTPException(
217
+ status_code=503,
218
+ detail=f"Ollama not available: {ollama_status.get('error', 'unknown')}"
219
+ )
220
+
221
+ try:
222
+ result = subprocess.run(
223
+ ["ollama", "run", request.model, request.prompt],
224
+ capture_output=True,
225
+ text=True,
226
+ timeout=120
227
+ )
228
+
229
+ if result.returncode != 0:
230
+ logger.error(f"[GENERATE] Ollama error: {result.stderr}")
231
+ raise HTTPException(
232
+ status_code=500,
233
+ detail=f"Ollama error: {result.stderr.strip()}"
234
+ )
235
+
236
+ response_text = result.stdout.strip()
237
+ logger.info(f"[GENERATE] Success, response_len={len(response_text)}")
238
+
239
+ return {
240
+ "model": request.model,
241
+ "response": response_text,
242
+ "done": True
243
+ }
244
+
245
+ except subprocess.TimeoutExpired:
246
+ logger.error("[GENERATE] Ollama timeout after 120s")
247
+ raise HTTPException(status_code=504, detail="Ollama request timed out after 120s")
248
+ except HTTPException:
249
+ raise
250
+ except Exception as e:
251
+ logger.error(f"[GENERATE] Unexpected error: {e}")
252
+ raise HTTPException(status_code=500, detail=str(e))
253
+
254
+
255
+ # --- File Search (called by file resolution) ---
256
+
257
  @app.post("/api/search", response_model=FileSearchResponse)
258
  async def search_file(request: FileSearchRequest, x_api_key: str = Header(None)):
259
+ """Fuzzy file matching for missing evidence files."""
260
  if not x_api_key or x_api_key != API_KEY:
261
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
262
+
263
  if len(request.missing_filename) > 200:
264
  return FileSearchResponse(
265
  status="error", missing_filename=request.missing_filename[:50] + "...",
266
  suggestions=[], confidence=0.0,
267
  reasoning="Filename too long - likely concatenated filenames"
268
  )
269
+
270
  matches = find_best_matches(request.missing_filename, request.available_files)
271
  confidence = matches[0]["score"] if matches else 0.0
272
+
273
  if not matches:
274
  reasoning = f"No matches found in {len(request.available_files)} files"
275
  elif matches[0]["match_type"] == "exact":
276
  reasoning = f"Exact match: {matches[0]['filename']}"
277
  else:
278
  reasoning = f"Token match with {int(confidence * 100)}% similarity"
279
+
280
  return FileSearchResponse(
281
  status="success", missing_filename=request.missing_filename,
282
  suggestions=matches, confidence=confidence, reasoning=reasoning
283
  )
284
 
285
+
286
+ # --- Report Analysis via JSON body ---
287
+
288
  @app.post("/api/analyze", response_model=AnalysisResponse)
289
+ async def analyze_report_json(request: AnalysisRequest, x_api_key: str = Header(None)):
290
+ """Analyze a verification report (JSON body). Called by API clients."""
291
  if not x_api_key or x_api_key != API_KEY:
292
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
293
+
294
+ result = analyze_report_data(request.report_data)
 
 
 
 
 
 
 
 
 
295
  return AnalysisResponse(
296
+ status=result["status"],
297
+ critical_issues=result["critical_issues"],
298
+ suggestions=result["suggestions"]
299
  )
300
 
301
+
302
+ # --- Report Analysis via file upload (called by trigger_cloud.py) ---
303
+
304
+ @app.post("/tools/analyze_report")
305
+ async def analyze_report_upload(
306
+ report_file: UploadFile = File(...),
307
+ x_api_key: str = Header(None)
308
+ ):
309
+ """Analyze a verification report uploaded as a file.
310
+ Called by trigger_cloud.py and generate_bundles_final_corrected.py cloud reporting.
311
+ """
312
+ if not x_api_key or x_api_key != API_KEY:
313
+ raise HTTPException(status_code=401, detail="Invalid or missing API Key")
314
+
315
+ logger.info(f"[ANALYZE_REPORT] Received file: {report_file.filename}")
316
+
317
+ try:
318
+ content = await report_file.read()
319
+ data = json.loads(content)
320
+ except json.JSONDecodeError:
321
+ raise HTTPException(status_code=400, detail="Invalid JSON in uploaded file")
322
+ except Exception as e:
323
+ raise HTTPException(status_code=400, detail=f"Error reading file: {str(e)}")
324
+
325
+ result = analyze_report_data(data)
326
+ logger.info(f"[ANALYZE_REPORT] Found {result['critical_issues']} critical issues")
327
+
328
+ return result
329
+
330
+
331
+ # --- Date Extraction ---
332
+
333
  @app.post("/api/extract_date")
334
  async def extract_date(filename: str, x_api_key: str = Header(None)):
335
+ """Extract date from a filename string."""
336
  if not x_api_key or x_api_key != API_KEY:
337
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
338
+
339
  patterns = [
340
  (r'(\d{4})-(\d{2})-(\d{2})', 'ISO'),
341
  (r'(\d{4})_(\d{2})_(\d{2})', 'underscore'),
342
+ (r'(\d{1,2})-(\d{1,2})-(\d{2,4})', 'UK_dash'),
343
  (r'(\d{4})(\d{2})(\d{2})', 'compact'),
344
  ]
345
  for pattern, fmt in patterns:
346
  match = re.search(pattern, filename)
347
  if match:
348
+ groups = match.groups()
349
+ if fmt == 'UK_dash':
350
+ day, month, year = groups
351
+ if len(year) == 2:
352
+ year = f"20{year}"
353
+ return {"status": "found", "date": f"{year}-{int(month):02d}-{int(day):02d}", "format": fmt}
354
+ else:
355
+ year, month, day = groups
356
+ return {"status": "found", "date": f"{year}-{int(month):02d}-{int(day):02d}", "format": fmt}
357
+
358
  return {"status": "not_found", "date": None}
359
 
360
+
361
  if __name__ == "__main__":
362
  import uvicorn
363
  uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
- # Moltbot Hybrid Engine - Safe Deployment
2
  fastapi>=0.104.0
3
  uvicorn>=0.24.0
4
  pydantic>=2.0.0
 
 
1
+ # Moltbot Hybrid Engine - Dependencies
2
  fastapi>=0.104.0
3
  uvicorn>=0.24.0
4
  pydantic>=2.0.0
5
+ python-multipart>=0.0.6
start.sh CHANGED
@@ -1,9 +1,49 @@
1
  #!/bin/bash
2
- # Moltbot Safe Startup - Simple version
3
- echo "🚀 Starting Moltbot Hybrid Engine (Safe Mode)..."
4
- echo " Version: 2.0.0"
5
- echo " Mode: Brain only, no file access"
6
- echo "=================================="
7
 
8
- # Start uvicorn directly (HF handles process management)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  exec python -m uvicorn app:app --host 0.0.0.0 --port 7860
 
1
  #!/bin/bash
2
+ # Moltbot Hybrid Engine - Multi-service Startup
3
+ # Starts: Ollama (background) + FastAPI/uvicorn (foreground on port 7860)
4
+ # Build: 2026-02-06
 
 
5
 
6
+ echo "============================================================"
7
+ echo " Moltbot Hybrid Engine v3.0.0 - Starting..."
8
+ echo "============================================================"
9
+ echo " Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
10
+ echo ""
11
+
12
+ # 1. Start Ollama in background
13
+ echo "[1/3] Starting Ollama server..."
14
+ ollama serve &
15
+ OLLAMA_PID=$!
16
+ echo " Ollama PID: $OLLAMA_PID"
17
+
18
+ # 2. Wait for Ollama to be ready (up to 30 seconds)
19
+ echo "[2/3] Waiting for Ollama to be ready..."
20
+ MAX_WAIT=30
21
+ WAITED=0
22
+ while [ $WAITED -lt $MAX_WAIT ]; do
23
+ if ollama list > /dev/null 2>&1; then
24
+ echo " Ollama ready after ${WAITED}s"
25
+ break
26
+ fi
27
+ sleep 2
28
+ WAITED=$((WAITED + 2))
29
+ done
30
+
31
+ if [ $WAITED -ge $MAX_WAIT ]; then
32
+ echo " WARNING: Ollama not ready after ${MAX_WAIT}s - FastAPI will start anyway"
33
+ echo " LLM endpoints will return 503 until Ollama is available"
34
+ else
35
+ # Try to pull model (non-blocking, in background)
36
+ echo " Checking for qwen2.5:7b model..."
37
+ if ! ollama list 2>/dev/null | grep -q "qwen2.5"; then
38
+ echo " Model not found, pulling in background..."
39
+ ollama pull qwen2.5:7b &
40
+ else
41
+ echo " Model already available"
42
+ fi
43
+ fi
44
+
45
+ # 3. Start FastAPI (foreground - this keeps the container alive)
46
+ echo "[3/3] Starting FastAPI on port 7860..."
47
+ echo "============================================================"
48
+ echo ""
49
  exec python -m uvicorn app:app --host 0.0.0.0 --port 7860