mirror of
https://github.com/myronblair/jarvis
synced 2026-07-29 09:12:36 -05:00
Add llava vision: use Ollama llava:7b for all image analysis, Claude as fallback
- Add _ollama_vision_call() using Ollama /api/generate with images array - handle_screenshot: try llava first (free, on-LAN), fall back to Claude on error; text-only snapshots now use ollama instead of groq - handle_vision: same llava-first/Claude-fallback pattern; caller can force provider='ollama' or 'claude' explicitly; stored provider_used reflects actual provider that ran Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
This commit is contained in:
+75
-44
@@ -175,6 +175,16 @@ async def _ollama_call(messages: list, system: str = "") -> str:
|
|||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
return data.get("response", "")
|
return data.get("response", "")
|
||||||
|
|
||||||
|
async def _ollama_vision_call(image_b64: str, prompt: str) -> str:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
f"{OLLAMA_HOST}/api/generate",
|
||||||
|
json={"model": "llava:7b", "prompt": prompt, "images": [image_b64], "stream": False},
|
||||||
|
timeout=aiohttp.ClientTimeout(total=120),
|
||||||
|
) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
return data.get("response", "").strip()
|
||||||
|
|
||||||
async def handle_llm(payload: dict) -> dict:
|
async def handle_llm(payload: dict) -> dict:
|
||||||
message = payload.get("message", "")
|
message = payload.get("message", "")
|
||||||
system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.")
|
system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.")
|
||||||
@@ -658,38 +668,44 @@ async def handle_screenshot(payload: dict) -> dict:
|
|||||||
height = result.get("height", 0)
|
height = result.get("height", 0)
|
||||||
file_size = result.get("file_size", 0)
|
file_size = result.get("file_size", 0)
|
||||||
|
|
||||||
# Run Claude vision analysis if we have an image
|
# Run vision analysis if we have an image — llava first, Claude fallback
|
||||||
analysis = ""
|
analysis = ""
|
||||||
provider_used = ""
|
provider_used = ""
|
||||||
if do_analyze and image_b64:
|
if do_analyze and image_b64:
|
||||||
try:
|
try:
|
||||||
import anthropic
|
analysis = await _ollama_vision_call(image_b64, analyze_prompt)
|
||||||
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
provider_used = "ollama:llava"
|
||||||
msg = await client.messages.create(
|
log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
|
||||||
model="claude-opus-4-8-20251101",
|
|
||||||
max_tokens=1024,
|
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "image", "source": {"type": "base64",
|
|
||||||
"media_type": "image/png", "data": image_b64}},
|
|
||||||
{"type": "text", "text": analyze_prompt},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
analysis = msg.content[0].text if msg.content else ""
|
|
||||||
provider_used = "claude"
|
|
||||||
log.info(f"[VISION] Claude analysis complete ({len(analysis)} chars)")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"[VISION] Claude vision failed: {e}")
|
log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
|
||||||
analysis = f"Vision analysis unavailable: {e}"
|
try:
|
||||||
|
import anthropic
|
||||||
|
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
||||||
|
msg = await client.messages.create(
|
||||||
|
model="claude-opus-4-8-20251101",
|
||||||
|
max_tokens=1024,
|
||||||
|
messages=[{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "image", "source": {"type": "base64",
|
||||||
|
"media_type": "image/png", "data": image_b64}},
|
||||||
|
{"type": "text", "text": analyze_prompt},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
analysis = msg.content[0].text if msg.content else ""
|
||||||
|
provider_used = "claude"
|
||||||
|
log.info(f"[VISION] Claude fallback analysis complete ({len(analysis)} chars)")
|
||||||
|
except Exception as e2:
|
||||||
|
log.warning(f"[VISION] Claude fallback also failed: {e2}")
|
||||||
|
analysis = f"Vision analysis unavailable: {e2}"
|
||||||
elif do_analyze and not image_b64 and result.get("snapshot_type") == "text":
|
elif do_analyze and not image_b64 and result.get("snapshot_type") == "text":
|
||||||
# Text-only sysinfo snapshot — summarize with LLM
|
# Text-only sysinfo snapshot — summarize with Ollama
|
||||||
try:
|
try:
|
||||||
snap_text = json.dumps(result, indent=2)[:3000]
|
snap_text = json.dumps(result, indent=2)[:3000]
|
||||||
prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}"
|
prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}"
|
||||||
analysis = await llm_call([{"role": "user", "content": prompt}], "groq")
|
analysis = await llm_call([{"role": "user", "content": prompt}], "ollama")
|
||||||
provider_used = "groq"
|
provider_used = "ollama"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
analysis = f"Analysis unavailable: {e}"
|
analysis = f"Analysis unavailable: {e}"
|
||||||
|
|
||||||
@@ -742,32 +758,47 @@ async def handle_vision(payload: dict) -> dict:
|
|||||||
if not image_b64:
|
if not image_b64:
|
||||||
raise ValueError("No image data provided")
|
raise ValueError("No image data provided")
|
||||||
|
|
||||||
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}")
|
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname} provider={provider}")
|
||||||
|
|
||||||
try:
|
analysis = ""
|
||||||
import anthropic
|
provider_used = provider
|
||||||
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
|
||||||
msg = await client.messages.create(
|
if provider == "ollama" or provider == "llava":
|
||||||
model="claude-opus-4-8-20251101",
|
analysis = await _ollama_vision_call(image_b64, prompt)
|
||||||
max_tokens=2048,
|
provider_used = "ollama:llava"
|
||||||
messages=[{
|
else:
|
||||||
"role": "user",
|
# Default: llava first, Claude fallback
|
||||||
"content": [
|
try:
|
||||||
{"type": "image", "source": {"type": "base64",
|
analysis = await _ollama_vision_call(image_b64, prompt)
|
||||||
"media_type": "image/png", "data": image_b64}},
|
provider_used = "ollama:llava"
|
||||||
{"type": "text", "text": prompt},
|
log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
|
||||||
],
|
except Exception as e:
|
||||||
}],
|
log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
|
||||||
)
|
try:
|
||||||
analysis = msg.content[0].text if msg.content else ""
|
import anthropic
|
||||||
except Exception as e:
|
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
||||||
raise RuntimeError(f"Vision analysis failed: {e}")
|
msg = await client.messages.create(
|
||||||
|
model="claude-opus-4-8-20251101",
|
||||||
|
max_tokens=2048,
|
||||||
|
messages=[{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "image", "source": {"type": "base64",
|
||||||
|
"media_type": "image/png", "data": image_b64}},
|
||||||
|
{"type": "text", "text": prompt},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
analysis = msg.content[0].text if msg.content else ""
|
||||||
|
provider_used = "claude"
|
||||||
|
except Exception as e2:
|
||||||
|
raise RuntimeError(f"Vision analysis failed (llava: {e}, claude: {e2})")
|
||||||
|
|
||||||
# Update stored screenshot if we have an ID
|
# Update stored screenshot if we have an ID
|
||||||
if screenshot_id:
|
if screenshot_id:
|
||||||
await db_execute(
|
await db_execute(
|
||||||
"UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s",
|
"UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s",
|
||||||
(analysis, "claude", int(screenshot_id))
|
(analysis, provider_used, int(screenshot_id))
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -775,7 +806,7 @@ async def handle_vision(payload: dict) -> dict:
|
|||||||
"screenshot_id": screenshot_id,
|
"screenshot_id": screenshot_id,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"analysis": analysis,
|
"analysis": analysis,
|
||||||
"provider": "claude",
|
"provider": provider_used,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user