dboa9 Cursor commited on
Commit
f6586bc
·
1 Parent(s): 134c04b

v6.0: Fix Ollama Exec format error + add HF Inference API fallback

Browse files

- Dockerfile: Fix Ollama binary download (use GitHub releases URL)
- app.py: Dual LLM backend - tries Ollama first, falls back to HF Inference API
- start.sh: Use qwen2.5:1.5b (fits free tier), validate binary before starting
- requirements.txt: Add huggingface_hub for Inference API
- Restore jira_adapter.py (was deleted)

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

Files changed (5) hide show
  1. Dockerfile +65 -0
  2. app.py +145 -61
  3. requirements.txt +2 -0
  4. shared/jira_adapter.py +295 -0
  5. start.sh +58 -41
Dockerfile ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Moltbot Hybrid Engine - Multi-service Dockerfile
2
+ # Runs: FastAPI (port 7860) + Ollama (optional, background)
3
+ # Build: 2026-02-08 v6.0
4
+ # FIX v6: Dual LLM backend - Ollama (if available) + HF Inference API fallback
5
+ # HF Inference API works on Free tier without GPU/Ollama
6
+
7
+ FROM python:3.11-slim
8
+
9
+ # Install packages required for HF Spaces Dev Mode + our needs
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ bash \
12
+ curl \
13
+ wget \
14
+ procps \
15
+ git \
16
+ git-lfs \
17
+ file \
18
+ && apt-get clean \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ # Install Ollama AS ROOT - force amd64 (HF Spaces run amd64)
22
+ # Download from GitHub releases (more reliable than ollama.com redirect)
23
+ # Mark as OPTIONAL - app works without it via HF Inference API fallback
24
+ RUN echo "Downloading Ollama (amd64)..." && \
25
+ curl -fSL --retry 3 --retry-delay 5 \
26
+ "https://github.com/ollama/ollama/releases/latest/download/ollama-linux-amd64" \
27
+ -o /usr/local/bin/ollama && \
28
+ chmod +x /usr/local/bin/ollama && \
29
+ echo "Ollama binary:" && file /usr/local/bin/ollama && \
30
+ echo "Size: $(du -h /usr/local/bin/ollama | cut -f1)" \
31
+ || echo "⚠️ Ollama download failed - will use HF Inference API only"
32
+
33
+ # Create HF-required user (uid 1000)
34
+ RUN useradd -m -u 1000 user
35
+
36
+ # Create Ollama model storage directory owned by user
37
+ RUN mkdir -p /home/user/ollama_models && chown -R user:user /home/user/ollama_models
38
+
39
+ # Switch to user
40
+ USER user
41
+ ENV HOME=/home/user \
42
+ PATH=/home/user/.local/bin:/usr/local/bin:$PATH \
43
+ OLLAMA_MODELS=/home/user/ollama_models \
44
+ OLLAMA_HOST=0.0.0.0
45
+
46
+ # Set working directory to /app (required for dev mode)
47
+ WORKDIR /app
48
+
49
+ # Upgrade pip
50
+ RUN pip install --no-cache-dir --upgrade pip
51
+
52
+ # Copy all files with correct ownership
53
+ COPY --chown=user . /app
54
+
55
+ # Install Python dependencies (includes huggingface_hub for Inference API)
56
+ RUN pip install --no-cache-dir -r requirements.txt
57
+
58
+ # Make start script executable
59
+ RUN chmod +x start.sh
60
+
61
+ # Expose HF Spaces port
62
+ EXPOSE 7860
63
+
64
+ # CMD required (not ENTRYPOINT) for dev mode compatibility
65
+ CMD ["./start.sh"]
app.py CHANGED
@@ -1,18 +1,22 @@
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
@@ -29,8 +33,8 @@ logger = logging.getLogger("moltbot-engine")
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
@@ -38,13 +42,19 @@ 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
 
@@ -123,11 +133,83 @@ def check_ollama_status() -> dict:
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 = []
@@ -142,7 +224,6 @@ def analyze_report_data(data: dict) -> dict:
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):
@@ -170,20 +251,36 @@ def health_check():
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
  }
@@ -200,59 +297,49 @@ def security_info():
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)):
@@ -287,7 +374,7 @@ async def search_file(request: FileSearchRequest, x_api_key: str = Header(None))
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
 
@@ -299,16 +386,14 @@ async def analyze_report_json(request: AnalysisRequest, x_api_key: str = Header(
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
 
@@ -324,7 +409,6 @@ async def analyze_report_upload(
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
 
 
1
  """
2
+ Moltbot Hybrid Engine - Production v6.0.0
3
+ Multi-service: FastAPI endpoints + Dual LLM backend (Ollama + HF Inference API)
4
  Runs on Hugging Face Spaces
5
+ Build: 2026-02-08
6
+
7
+ LLM Strategy:
8
+ 1. Try Ollama (local, if installed and running)
9
+ 2. Fallback to HuggingFace Inference API (always available, no GPU needed)
10
 
11
  Endpoints:
12
  GET / - Health check
13
  GET /health - Detailed health status
14
  GET /security - Security posture info
15
+ POST /api/generate - LLM text generation (Ollama HF Inference API fallback)
16
+ POST /api/search - Fuzzy file matching
17
  POST /api/analyze - Report analysis (JSON body)
18
  POST /api/extract_date - Date extraction from filenames
19
+ POST /tools/analyze_report - Report analysis via file upload
20
  """
21
  import os
22
  import re
 
33
  # Initialize App
34
  app = FastAPI(
35
  title="Moltbot Hybrid Engine",
36
+ description="AI agent for legal document processing - Dual LLM + file matching + analysis",
37
+ version="6.0.0"
38
  )
39
 
40
  # API Key for authentication
 
42
  if API_KEY == "default_insecure_key":
43
  logger.warning("MOLTBOT_API_KEY not set. Using insecure default.")
44
 
45
+ # HuggingFace token for Inference API
46
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
47
+
48
+ # Default HF model for inference API fallback
49
+ HF_MODEL = os.environ.get("HF_MODEL", "Qwen/Qwen2.5-7B-Instruct")
50
+
51
 
52
  # ============================================================
53
  # DATA MODELS
54
  # ============================================================
55
 
56
  class GenerateRequest(BaseModel):
57
+ model: str = "qwen2.5:1.5b"
58
  prompt: str
59
  stream: bool = False
60
 
 
133
  return {"running": False, "error": result.stderr.strip()}
134
  except FileNotFoundError:
135
  return {"running": False, "error": "ollama binary not found"}
136
+ except OSError as e:
137
+ return {"running": False, "error": f"ollama exec error: {e}"}
138
  except subprocess.TimeoutExpired:
139
  return {"running": False, "error": "ollama list timed out"}
140
  except Exception as e:
141
  return {"running": False, "error": str(e)}
142
 
143
+
144
+ def generate_with_ollama(model: str, prompt: str) -> Optional[str]:
145
+ """Try to generate text with local Ollama. Returns None if unavailable."""
146
+ try:
147
+ result = subprocess.run(
148
+ ["ollama", "run", model, prompt],
149
+ capture_output=True, text=True, timeout=120
150
+ )
151
+ if result.returncode == 0 and result.stdout.strip():
152
+ return result.stdout.strip()
153
+ logger.warning(f"[OLLAMA] Non-zero return or empty output: {result.stderr[:200]}")
154
+ return None
155
+ except (FileNotFoundError, OSError) as e:
156
+ logger.warning(f"[OLLAMA] Not available: {e}")
157
+ return None
158
+ except subprocess.TimeoutExpired:
159
+ logger.warning("[OLLAMA] Timeout after 120s")
160
+ return None
161
+ except Exception as e:
162
+ logger.warning(f"[OLLAMA] Error: {e}")
163
+ return None
164
+
165
+
166
+ def generate_with_hf_api(prompt: str, model: str = None) -> Optional[str]:
167
+ """Generate text using HuggingFace Inference API (free, no GPU needed)."""
168
+ try:
169
+ from huggingface_hub import InferenceClient
170
+
171
+ hf_model = model or HF_MODEL
172
+ token = HF_TOKEN if HF_TOKEN else None
173
+
174
+ client = InferenceClient(token=token)
175
+
176
+ # Use text_generation for instruct models
177
+ response = client.text_generation(
178
+ prompt=prompt,
179
+ model=hf_model,
180
+ max_new_tokens=1024,
181
+ temperature=0.7,
182
+ do_sample=True,
183
+ )
184
+
185
+ if response:
186
+ return response.strip()
187
+
188
+ logger.warning("[HF_API] Empty response")
189
+ return None
190
+
191
+ except ImportError:
192
+ logger.error("[HF_API] huggingface_hub not installed")
193
+ return None
194
+ except Exception as e:
195
+ logger.warning(f"[HF_API] Error: {e}")
196
+ # Try chat completion as fallback
197
+ try:
198
+ from huggingface_hub import InferenceClient
199
+ client = InferenceClient(token=HF_TOKEN if HF_TOKEN else None)
200
+ response = client.chat_completion(
201
+ model=model or HF_MODEL,
202
+ messages=[{"role": "user", "content": prompt}],
203
+ max_tokens=1024,
204
+ temperature=0.7,
205
+ )
206
+ if response and response.choices:
207
+ return response.choices[0].message.content.strip()
208
+ except Exception as e2:
209
+ logger.warning(f"[HF_API] Chat completion also failed: {e2}")
210
+ return None
211
+
212
+
213
  def analyze_report_data(data: dict) -> dict:
214
  """Analyze a verification report and return findings."""
215
  suggestions = []
 
224
  if blank_pages > 0:
225
  suggestions.append(f"{blank_pages} blank placeholder pages found - files listed in TOC but not embedded")
226
 
 
227
  bundles = data.get("bundles", {})
228
  for bundle_name, bundle_data in bundles.items():
229
  if isinstance(bundle_data, dict):
 
251
  return {
252
  "status": "running",
253
  "service": "Moltbot Hybrid Engine",
254
+ "version": "6.0.0",
255
+ "ollama": ollama,
256
+ "hf_inference_api": {
257
+ "available": True,
258
+ "model": HF_MODEL,
259
+ "token_set": bool(HF_TOKEN)
260
+ }
261
  }
262
 
263
  @app.get("/health")
264
  def detailed_health():
265
+ """Detailed health check with LLM status."""
266
  ollama = check_ollama_status()
267
  return {
268
  "status": "healthy",
269
  "service": "moltbot-hybrid-engine",
270
+ "version": "6.0.0",
271
+ "llm_backends": {
272
+ "ollama": {
273
+ "running": ollama.get("running", False),
274
+ "models": ollama.get("models", []),
275
+ "error": ollama.get("error"),
276
+ },
277
+ "hf_inference_api": {
278
+ "available": True,
279
+ "model": HF_MODEL,
280
+ "token_set": bool(HF_TOKEN),
281
+ "note": "Always available as fallback, no GPU needed"
282
+ }
283
+ },
284
  "endpoints": ["/", "/health", "/api/generate", "/api/search",
285
  "/api/analyze", "/api/extract_date", "/tools/analyze_report"]
286
  }
 
297
  }
298
 
299
 
300
+ # --- LLM Generation (Dual Backend: Ollama → HF Inference API) ---
301
 
302
  @app.post("/api/generate")
303
  async def generate(request: GenerateRequest, x_api_key: str = Header(None)):
304
+ """Generate text using LLM. Tries Ollama first, falls back to HF Inference API."""
305
  if not x_api_key or x_api_key != API_KEY:
306
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
307
 
308
  logger.info(f"[GENERATE] model={request.model}, prompt_len={len(request.prompt)}")
309
 
310
+ backend_used = None
311
+ response_text = None
312
+
313
+ # Backend 1: Try Ollama (local)
314
+ response_text = generate_with_ollama(request.model, request.prompt)
315
+ if response_text:
316
+ backend_used = "ollama"
317
+ logger.info(f"[GENERATE] Ollama success, response_len={len(response_text)}")
318
+
319
+ # Backend 2: Fallback to HF Inference API
320
+ if not response_text:
321
+ logger.info("[GENERATE] Ollama unavailable, trying HF Inference API...")
322
+ response_text = generate_with_hf_api(request.prompt)
323
+ if response_text:
324
+ backend_used = "hf_inference_api"
325
+ logger.info(f"[GENERATE] HF API success, response_len={len(response_text)}")
326
+
327
+ # Both failed
328
+ if not response_text:
329
  raise HTTPException(
330
  status_code=503,
331
+ detail="Both LLM backends unavailable. Ollama not running + HF Inference API failed. Check HF_TOKEN."
332
  )
333
 
334
+ return {
335
+ "model": request.model,
336
+ "response": response_text,
337
+ "backend": backend_used,
338
+ "done": True
339
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
 
342
+ # --- File Search ---
343
 
344
  @app.post("/api/search", response_model=FileSearchResponse)
345
  async def search_file(request: FileSearchRequest, x_api_key: str = Header(None)):
 
374
 
375
  @app.post("/api/analyze", response_model=AnalysisResponse)
376
  async def analyze_report_json(request: AnalysisRequest, x_api_key: str = Header(None)):
377
+ """Analyze a verification report (JSON body)."""
378
  if not x_api_key or x_api_key != API_KEY:
379
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
380
 
 
386
  )
387
 
388
 
389
+ # --- Report Analysis via file upload ---
390
 
391
  @app.post("/tools/analyze_report")
392
  async def analyze_report_upload(
393
  report_file: UploadFile = File(...),
394
  x_api_key: str = Header(None)
395
  ):
396
+ """Analyze a verification report uploaded as a file."""
 
 
397
  if not x_api_key or x_api_key != API_KEY:
398
  raise HTTPException(status_code=401, detail="Invalid or missing API Key")
399
 
 
409
 
410
  result = analyze_report_data(data)
411
  logger.info(f"[ANALYZE_REPORT] Found {result['critical_issues']} critical issues")
 
412
  return result
413
 
414
 
requirements.txt CHANGED
@@ -3,3 +3,5 @@ fastapi>=0.104.0
3
  uvicorn>=0.24.0
4
  pydantic>=2.0.0
5
  python-multipart>=0.0.6
 
 
 
3
  uvicorn>=0.24.0
4
  pydantic>=2.0.0
5
  python-multipart>=0.0.6
6
+ huggingface_hub>=0.20.0
7
+ requests>=2.31.0
shared/jira_adapter.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ JIRA & CONFLUENCE ADAPTER
3
+ =========================
4
+ Zero-risk adapter for Jira and Confluence integration.
5
+ Loads credentials from environment variables or config files.
6
+
7
+ Usage:
8
+ from shared import JiraAdapter, ConfluenceAdapter
9
+
10
+ jira = JiraAdapter()
11
+ jira.add_comment("COURT-123", "Bundle generated successfully")
12
+
13
+ confluence = ConfluenceAdapter()
14
+ confluence.update_page("123456", "New content")
15
+
16
+ Created: 2026-02-04
17
+ """
18
+
19
+ import os
20
+ import re
21
+ from pathlib import Path
22
+ from typing import Optional, Dict, Any
23
+
24
+ # Try to import jira library
25
+ try:
26
+ from jira import JIRA
27
+ JIRA_AVAILABLE = True
28
+ except ImportError:
29
+ JIRA = None
30
+ JIRA_AVAILABLE = False
31
+
32
+ # Try to import atlassian library for Confluence
33
+ try:
34
+ from atlassian import Confluence
35
+ CONFLUENCE_AVAILABLE = True
36
+ except ImportError:
37
+ Confluence = None
38
+ CONFLUENCE_AVAILABLE = False
39
+
40
+
41
+ def _load_config_from_files() -> Dict[str, str]:
42
+ """
43
+ Load configuration from ~/.bashrc or ~/.secure/api_keys.
44
+ Returns dict with JIRA_URL, JIRA_EMAIL, JIRA_TOKEN, CONFLUENCE_URL.
45
+ """
46
+ config = {}
47
+
48
+ # Files to search for credentials
49
+ config_files = [
50
+ Path.home() / ".bashrc",
51
+ Path.home() / ".secure" / "api_keys",
52
+ Path.home() / ".env",
53
+ ]
54
+
55
+ patterns = {
56
+ 'JIRA_URL': r'export\s+JIRA_URL\s*=\s*["\']?([^"\';\n]+)',
57
+ 'JIRA_EMAIL': r'export\s+JIRA_EMAIL\s*=\s*["\']?([^"\';\n]+)',
58
+ 'JIRA_TOKEN': r'export\s+JIRA_TOKEN\s*=\s*["\']?([^"\';\n]+)',
59
+ 'CONFLUENCE_URL': r'export\s+CONFLUENCE_URL\s*=\s*["\']?([^"\';\n]+)',
60
+ }
61
+
62
+ for config_file in config_files:
63
+ if config_file.exists():
64
+ try:
65
+ content = config_file.read_text()
66
+ for key, pattern in patterns.items():
67
+ if key not in config or not config[key]:
68
+ match = re.search(pattern, content)
69
+ if match:
70
+ config[key] = match.group(1).strip()
71
+ except Exception:
72
+ continue
73
+
74
+ return config
75
+
76
+
77
+ class JiraAdapter:
78
+ """
79
+ Robust Jira adapter with automatic credential loading.
80
+ """
81
+
82
+ def __init__(self, url: str = None, email: str = None, token: str = None):
83
+ """
84
+ Initialize Jira connection.
85
+ Credentials loaded from: args > env vars > config files
86
+ """
87
+ # Load from config files first as fallback
88
+ file_config = _load_config_from_files()
89
+
90
+ # Priority: args > env vars > config files
91
+ self.url = url or os.environ.get('JIRA_URL') or file_config.get('JIRA_URL')
92
+ self.email = email or os.environ.get('JIRA_EMAIL') or file_config.get('JIRA_EMAIL')
93
+ self.token = token or os.environ.get('JIRA_TOKEN') or file_config.get('JIRA_TOKEN')
94
+
95
+ self.client = None
96
+ self.connected = False
97
+ self.error_message = None
98
+
99
+ # Attempt connection
100
+ if self.url and self.email and self.token and JIRA_AVAILABLE:
101
+ try:
102
+ self.client = JIRA(
103
+ server=self.url,
104
+ basic_auth=(self.email, self.token)
105
+ )
106
+ self.connected = True
107
+ print(f"✅ [JiraAdapter] Connected to {self.url}")
108
+ except Exception as e:
109
+ self.error_message = str(e)
110
+ print(f"⚠️ [JiraAdapter] Connection failed: {e}")
111
+ else:
112
+ missing = []
113
+ if not self.url:
114
+ missing.append('JIRA_URL')
115
+ if not self.email:
116
+ missing.append('JIRA_EMAIL')
117
+ if not self.token:
118
+ missing.append('JIRA_TOKEN')
119
+ if not JIRA_AVAILABLE:
120
+ missing.append('jira library')
121
+ self.error_message = f"Missing: {', '.join(missing)}"
122
+ print(f"⚠️ [JiraAdapter] Not configured: {self.error_message}")
123
+
124
+ def add_comment(self, issue_key: str, comment: str) -> bool:
125
+ """Add a comment to a Jira issue."""
126
+ if not self.connected:
127
+ print(f"⚠️ [JiraAdapter] Cannot add comment - not connected")
128
+ return False
129
+
130
+ try:
131
+ self.client.add_comment(issue_key, comment)
132
+ print(f"✅ [JiraAdapter] Comment added to {issue_key}")
133
+ return True
134
+ except Exception as e:
135
+ print(f"❌ [JiraAdapter] Failed to add comment to {issue_key}: {e}")
136
+ return False
137
+
138
+ def update_status(self, issue_key: str, status: str) -> bool:
139
+ """Update the status of a Jira issue."""
140
+ if not self.connected:
141
+ return False
142
+
143
+ try:
144
+ transitions = self.client.transitions(issue_key)
145
+ for t in transitions:
146
+ if t['name'].lower() == status.lower():
147
+ self.client.transition_issue(issue_key, t['id'])
148
+ print(f"✅ [JiraAdapter] Status updated to '{status}' for {issue_key}")
149
+ return True
150
+ print(f"⚠️ [JiraAdapter] Status '{status}' not found for {issue_key}")
151
+ return False
152
+ except Exception as e:
153
+ print(f"❌ [JiraAdapter] Failed to update status: {e}")
154
+ return False
155
+
156
+ def create_issue(self, project: str, summary: str, description: str = "",
157
+ issue_type: str = "Task") -> Optional[str]:
158
+ """Create a new Jira issue. Returns issue key or None."""
159
+ if not self.connected:
160
+ return None
161
+
162
+ try:
163
+ issue = self.client.create_issue(
164
+ project=project,
165
+ summary=summary,
166
+ description=description,
167
+ issuetype={'name': issue_type}
168
+ )
169
+ print(f"✅ [JiraAdapter] Created issue {issue.key}")
170
+ return issue.key
171
+ except Exception as e:
172
+ print(f"❌ [JiraAdapter] Failed to create issue: {e}")
173
+ return None
174
+
175
+
176
+ def is_connected(self) -> bool:
177
+ """Check if JIRA connection is active (Added by Auto-Fix)."""
178
+ return self.connected and self.client is not None
179
+
180
+ def get_issue(self, issue_key: str) -> Optional[Dict[str, Any]]:
181
+ """Get issue details."""
182
+ if not self.connected:
183
+ return None
184
+
185
+ try:
186
+ issue = self.client.issue(issue_key)
187
+ return {
188
+ 'key': issue.key,
189
+ 'summary': issue.fields.summary,
190
+ 'status': issue.fields.status.name,
191
+ 'description': issue.fields.description
192
+ }
193
+ except Exception as e:
194
+ print(f"❌ [JiraAdapter] Failed to get issue {issue_key}: {e}")
195
+ return None
196
+
197
+
198
+ class ConfluenceAdapter:
199
+ """
200
+ Robust Confluence adapter with automatic credential loading.
201
+ """
202
+
203
+ def __init__(self, url: str = None, email: str = None, token: str = None):
204
+ """
205
+ Initialize Confluence connection.
206
+ """
207
+ file_config = _load_config_from_files()
208
+
209
+ self.url = url or os.environ.get('CONFLUENCE_URL') or file_config.get('CONFLUENCE_URL')
210
+ self.email = email or os.environ.get('JIRA_EMAIL') or file_config.get('JIRA_EMAIL')
211
+ self.token = token or os.environ.get('JIRA_TOKEN') or file_config.get('JIRA_TOKEN')
212
+
213
+ self.client = None
214
+ self.connected = False
215
+
216
+ if self.url and self.email and self.token and CONFLUENCE_AVAILABLE:
217
+ try:
218
+ self.client = Confluence(
219
+ url=self.url,
220
+ username=self.email,
221
+ password=self.token,
222
+ cloud=True
223
+ )
224
+ self.connected = True
225
+ print(f"✅ [ConfluenceAdapter] Connected to {self.url}")
226
+ except Exception as e:
227
+ print(f"⚠️ [ConfluenceAdapter] Connection failed: {e}")
228
+ else:
229
+ print(f"⚠️ [ConfluenceAdapter] Not configured")
230
+
231
+ def is_connected(self) -> bool:
232
+ """Check if Confluence connection is active."""
233
+ return self.connected and self.client is not None
234
+
235
+ def update_page(self, page_id: str, content: str, title: str = None) -> bool:
236
+ """Update a Confluence page."""
237
+ if not self.connected:
238
+ return False
239
+
240
+ try:
241
+ page = self.client.get_page_by_id(page_id)
242
+ current_title = title or page['title']
243
+
244
+ self.client.update_page(
245
+ page_id=page_id,
246
+ title=current_title,
247
+ body=content
248
+ )
249
+ print(f"✅ [ConfluenceAdapter] Updated page {page_id}")
250
+ return True
251
+ except Exception as e:
252
+ print(f"❌ [ConfluenceAdapter] Failed to update page: {e}")
253
+ return False
254
+
255
+ def get_page_content(self, page_id: str) -> Optional[str]:
256
+ """Get Confluence page content."""
257
+ if not self.connected:
258
+ return None
259
+
260
+ try:
261
+ page = self.client.get_page_by_id(page_id, expand='body.storage')
262
+ return page['body']['storage']['value']
263
+ except Exception as e:
264
+ print(f"❌ [ConfluenceAdapter] Failed to get page: {e}")
265
+ return None
266
+
267
+
268
+ def diagnose():
269
+ """Run diagnostic check on Jira/Confluence connectivity."""
270
+ print("=" * 60)
271
+ print("JIRA/CONFLUENCE ADAPTER DIAGNOSTIC")
272
+ print("=" * 60)
273
+
274
+ config = _load_config_from_files()
275
+ print(f"\nConfig from files:")
276
+ for k, v in config.items():
277
+ masked = v[:10] + "..." if v and len(v) > 10 else v
278
+ print(f" {k}: {masked}")
279
+
280
+ print(f"\nJira library available: {JIRA_AVAILABLE}")
281
+ print(f"Confluence library available: {CONFLUENCE_AVAILABLE}")
282
+
283
+ print("\nTesting Jira connection...")
284
+ jira = JiraAdapter()
285
+ print(f" Connected: {jira.connected}")
286
+
287
+ print("\nTesting Confluence connection...")
288
+ confluence = ConfluenceAdapter()
289
+ print(f" Connected: {confluence.connected}")
290
+
291
+ print("=" * 60)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ diagnose()
start.sh CHANGED
@@ -1,12 +1,11 @@
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 v4.0
5
- # Ollama is pre-installed in Dockerfile (as root)
6
- # This script just starts it and pulls the model
7
 
8
  echo "============================================================"
9
- echo " Moltbot Hybrid Engine v4.0.0 - Starting..."
10
  echo "============================================================"
11
  echo " Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
12
  echo " User: $(whoami) | Home: $HOME"
@@ -16,53 +15,71 @@ echo ""
16
  export OMP_NUM_THREADS=2
17
  export MKL_NUM_THREADS=2
18
 
19
- # 1. Verify Ollama is installed (was installed in Dockerfile)
20
  echo "[1/4] Checking Ollama installation..."
 
21
  if command -v ollama &> /dev/null; then
22
- echo " ✅ Ollama installed: $(ollama --version 2>/dev/null || echo 'present')"
 
 
 
 
 
 
 
 
23
  else
24
- echo " Ollama binary not found - this should not happen"
25
- echo " Attempting emergency install..."
26
- curl -fsSL https://ollama.com/install.sh | sh 2>&1 | tail -3
27
  fi
28
 
29
- # 2. Start Ollama server in background
30
- echo "[2/4] Starting Ollama server..."
31
- ollama serve &
32
- OLLAMA_PID=$!
33
- echo " Ollama PID: $OLLAMA_PID"
 
34
 
35
- # 3. Wait for Ollama to be ready (up to 30 seconds)
36
- echo "[3/4] Waiting for Ollama to be ready..."
37
- MAX_WAIT=30
38
- WAITED=0
39
- while [ $WAITED -lt $MAX_WAIT ]; do
40
- if curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then
41
- echo " ✅ Ollama ready after ${WAITED}s"
42
- break
43
- fi
44
- sleep 1
45
- WAITED=$((WAITED + 1))
46
- done
47
 
48
- if [ $WAITED -ge $MAX_WAIT ]; then
49
- echo " ⚠️ Ollama not ready after ${MAX_WAIT}s"
50
- echo " LLM endpoints will return 503 until Ollama is available"
51
- else
52
- # Pull model in background so FastAPI starts immediately
53
- echo " Checking for qwen2.5:7b model..."
54
- if ! ollama list 2>/dev/null | grep -q "qwen2.5"; then
55
- echo " ⏳ Model not found, pulling qwen2.5:7b in background..."
56
- echo " (This takes 3-5 minutes on first run, model will be cached after)"
57
- nohup ollama pull qwen2.5:7b > /tmp/ollama_pull.log 2>&1 &
58
- PULL_PID=$!
59
- echo " Pull PID: $PULL_PID (check /tmp/ollama_pull.log for progress)"
60
  else
61
- echo " ✅ Model already available"
 
 
 
 
 
 
 
 
 
 
62
  fi
 
 
 
63
  fi
64
 
65
- # 4. Start FastAPI (foreground - this keeps the container alive)
 
 
 
 
 
66
  echo "[4/4] Starting FastAPI on port 7860..."
67
  echo "============================================================"
68
  echo ""
 
1
  #!/bin/bash
2
  # Moltbot Hybrid Engine - Multi-service Startup
3
+ # Starts: Ollama (background, optional) + FastAPI/uvicorn (foreground on port 7860)
4
+ # Build: 2026-02-08 v6.0
5
+ # v6: Ollama is optional - HF Inference API provides fallback
 
6
 
7
  echo "============================================================"
8
+ echo " Moltbot Hybrid Engine v6.0.0 - Starting..."
9
  echo "============================================================"
10
  echo " Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
11
  echo " User: $(whoami) | Home: $HOME"
 
15
  export OMP_NUM_THREADS=2
16
  export MKL_NUM_THREADS=2
17
 
18
+ # 1. Check if Ollama binary exists and is valid
19
  echo "[1/4] Checking Ollama installation..."
20
+ OLLAMA_OK=false
21
  if command -v ollama &> /dev/null; then
22
+ # Verify binary is executable (not wrong architecture)
23
+ if file "$(which ollama)" 2>/dev/null | grep -q "ELF.*x86-64"; then
24
+ echo " ✅ Ollama binary verified (x86-64 ELF)"
25
+ OLLAMA_OK=true
26
+ else
27
+ echo " ⚠️ Ollama binary exists but may be wrong architecture"
28
+ echo " Binary type: $(file "$(which ollama)" 2>/dev/null)"
29
+ echo " → Will use HF Inference API fallback"
30
+ fi
31
  else
32
+ echo " ⚠️ Ollama binary not found"
33
+ echo " Will use HF Inference API fallback"
 
34
  fi
35
 
36
+ # 2. Start Ollama server in background (if binary is OK)
37
+ if [ "$OLLAMA_OK" = true ]; then
38
+ echo "[2/4] Starting Ollama server..."
39
+ ollama serve &
40
+ OLLAMA_PID=$!
41
+ echo " Ollama PID: $OLLAMA_PID"
42
 
43
+ # 3. Wait for Ollama to be ready (up to 20 seconds)
44
+ echo "[3/4] Waiting for Ollama to be ready..."
45
+ MAX_WAIT=20
46
+ WAITED=0
47
+ while [ $WAITED -lt $MAX_WAIT ]; do
48
+ if curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then
49
+ echo " ✅ Ollama ready after ${WAITED}s"
50
+ break
51
+ fi
52
+ sleep 1
53
+ WAITED=$((WAITED + 1))
54
+ done
55
 
56
+ if [ $WAITED -ge $MAX_WAIT ]; then
57
+ echo " ⚠️ Ollama not ready after ${MAX_WAIT}s"
58
+ echo " LLM will use HF Inference API fallback"
 
 
 
 
 
 
 
 
 
59
  else
60
+ # Pull small model in background (1.5b for free tier)
61
+ echo " Checking for qwen2.5:1.5b model..."
62
+ if ! ollama list 2>/dev/null | grep -q "qwen2.5"; then
63
+ echo " ⏳ Model not found, pulling qwen2.5:1.5b in background..."
64
+ echo " (Takes 1-2 minutes, model cached after first pull)"
65
+ nohup ollama pull qwen2.5:1.5b > /tmp/ollama_pull.log 2>&1 &
66
+ PULL_PID=$!
67
+ echo " Pull PID: $PULL_PID"
68
+ else
69
+ echo " ✅ Model already available"
70
+ fi
71
  fi
72
+ else
73
+ echo "[2/4] Skipping Ollama (not available)"
74
+ echo "[3/4] Skipping Ollama model pull"
75
  fi
76
 
77
+ echo ""
78
+ echo " 💡 HF Inference API fallback is always available"
79
+ echo " (Uses Qwen/Qwen2.5-7B-Instruct hosted by HuggingFace)"
80
+ echo ""
81
+
82
+ # 4. Start FastAPI (foreground - keeps container alive)
83
  echo "[4/4] Starting FastAPI on port 7860..."
84
  echo "============================================================"
85
  echo ""