Spaces:
Running
Running
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>
- Dockerfile +65 -0
- app.py +145 -61
- requirements.txt +2 -0
- shared/jira_adapter.py +295 -0
- 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
|
| 3 |
-
Multi-service: FastAPI endpoints +
|
| 4 |
Runs on Hugging Face Spaces
|
| 5 |
-
Build: 2026-02-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 12 |
-
POST /api/search - Fuzzy file matching
|
| 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
|
| 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="
|
| 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:
|
| 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": "
|
| 174 |
-
"ollama": ollama
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
@app.get("/health")
|
| 178 |
def detailed_health():
|
| 179 |
-
"""Detailed health check with
|
| 180 |
ollama = check_ollama_status()
|
| 181 |
return {
|
| 182 |
"status": "healthy",
|
| 183 |
"service": "moltbot-hybrid-engine",
|
| 184 |
-
"version": "
|
| 185 |
-
"
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 204 |
|
| 205 |
@app.post("/api/generate")
|
| 206 |
async def generate(request: GenerateRequest, x_api_key: str = Header(None)):
|
| 207 |
-
"""Generate text using
|
| 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 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
raise HTTPException(
|
| 217 |
status_code=503,
|
| 218 |
-
detail=
|
| 219 |
)
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 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
|
| 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).
|
| 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
|
| 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-
|
| 5 |
-
# Ollama is
|
| 6 |
-
# This script just starts it and pulls the model
|
| 7 |
|
| 8 |
echo "============================================================"
|
| 9 |
-
echo " Moltbot Hybrid Engine
|
| 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.
|
| 20 |
echo "[1/4] Checking Ollama installation..."
|
|
|
|
| 21 |
if command -v ollama &> /dev/null; then
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
else
|
| 24 |
-
echo "
|
| 25 |
-
echo "
|
| 26 |
-
curl -fsSL https://ollama.com/install.sh | sh 2>&1 | tail -3
|
| 27 |
fi
|
| 28 |
|
| 29 |
-
# 2. Start Ollama server in background
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
|
| 35 |
-
# 3. Wait for Ollama to be ready (up to
|
| 36 |
-
echo "[3/4] Waiting for Ollama to be ready..."
|
| 37 |
-
MAX_WAIT=
|
| 38 |
-
WAITED=0
|
| 39 |
-
while [ $WAITED -lt $MAX_WAIT ]; do
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
done
|
| 47 |
|
| 48 |
-
if [ $WAITED -ge $MAX_WAIT ]; then
|
| 49 |
-
|
| 50 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
fi
|
|
|
|
|
|
|
|
|
|
| 63 |
fi
|
| 64 |
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ""
|