agromatrix: deploy context/photo learning + deterministic excel policy

This commit is contained in:
NODA1 System
2026-02-21 10:56:20 +01:00
parent 815a287474
commit a91309de11
4 changed files with 194 additions and 524 deletions

View File

@@ -11,7 +11,6 @@ import httpx
import logging
import hashlib
import time # For latency metrics
from datetime import datetime
# CrewAI Integration
try:
@@ -236,18 +235,6 @@ def _build_image_fallback_response(agent_id: str, prompt: str = "") -> str:
return "Я поки не бачу достатньо деталей на фото. Надішли, будь ласка, чіткіше фото або крупний план об'єкта."
def _parse_tool_json_result(raw: Any) -> Dict[str, Any]:
if isinstance(raw, dict):
return raw
if isinstance(raw, str):
try:
parsed = json.loads(raw)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
return {}
def _looks_like_image_question(prompt: str) -> bool:
if not prompt:
@@ -1351,32 +1338,20 @@ async def agent_infer(agent_id: str, request: InferRequest):
logger.info(f" No system_prompt in request for agent {agent_id}, loading from configured sources")
if not system_prompt:
if not (CITY_SERVICE_URL or '').strip():
try:
from prompt_builder import get_agent_system_prompt
system_prompt = await get_agent_system_prompt(
agent_id,
city_service_url=CITY_SERVICE_URL,
router_config=router_config
)
logger.info(f"✅ Loaded system prompt from database for {agent_id}")
except Exception as e:
logger.warning(f"⚠️ Could not load prompt from database: {e}")
# Fallback to config
system_prompt_source = "router_config"
agent_config = router_config.get("agents", {}).get(agent_id, {})
system_prompt = agent_config.get("system_prompt")
logger.info(f" CITY_SERVICE_URL is empty; loaded system prompt from router_config for {agent_id}")
else:
try:
from prompt_builder import get_agent_system_prompt
system_prompt = await get_agent_system_prompt(
agent_id,
city_service_url=CITY_SERVICE_URL,
router_config=router_config
)
logger.info(f"✅ Loaded system prompt from city service/config for {agent_id}")
except Exception as e:
logger.warning(f"⚠️ Could not load prompt via prompt_builder: {e}")
# Fallback to config
system_prompt_source = "router_config"
agent_config = router_config.get("agents", {}).get(agent_id, {})
system_prompt = agent_config.get("system_prompt")
if system_prompt and system_prompt_source == "city_service":
# prompt_builder may silently fall back to router config; reflect actual source in metadata/logs
cfg_prompt = (router_config.get("agents", {}).get(agent_id, {}) or {}).get("system_prompt")
if cfg_prompt and (system_prompt or "").strip() == str(cfg_prompt).strip():
system_prompt_source = "router_config"
if not system_prompt:
system_prompt_source = "empty"
@@ -1399,109 +1374,6 @@ async def agent_infer(agent_id: str, request: InferRequest):
# Use router config to get default model for agent, fallback to qwen3:8b
agent_config = router_config.get("agents", {}).get(agent_id, {})
# =========================================================================
# AGROMATRIX PLANT PRE-VISION (edge tool before CrewAI)
# =========================================================================
crewai_profile = str(effective_metadata.get("crewai_profile", "") or "").strip().lower()
is_agromatrix_plant = request_agent_id == "agromatrix" and crewai_profile == "plant_intel"
if is_agromatrix_plant and http_client and user_id and chat_id and not request.images:
# Follow-up path: reuse last structured plant identification from fact-memory.
fact_key = f"last_plant:{request_agent_id}:{chat_id}"
try:
fact_resp = await http_client.get(
f"http://memory-service:8000/facts/{fact_key}",
params={"user_id": user_id},
timeout=8.0,
)
if fact_resp.status_code == 200:
fact_data = fact_resp.json() or {}
last_plant = fact_data.get("fact_value_json") or {}
if isinstance(last_plant, str):
try:
last_plant = json.loads(last_plant)
except Exception:
last_plant = {}
if isinstance(last_plant, dict) and last_plant.get("top_k"):
effective_metadata["last_plant"] = last_plant
# Give deterministic context to synthesis without exposing internals to end user.
request.prompt = (
f"{request.prompt}\n\n"
f"[PREVIOUS_PLANT_IDENTIFICATION] {json.dumps(last_plant, ensure_ascii=False)}"
)
logger.info(
f"🌿 Plant follow-up context loaded: top1={((last_plant.get('top_k') or [{}])[0]).get('scientific_name', 'N/A')}"
)
except Exception as e:
logger.warning(f"⚠️ Plant follow-up context load failed: {e}")
if is_agromatrix_plant and request.images and len(request.images) > 0 and TOOL_MANAGER_AVAILABLE and tool_manager:
first_image = request.images[0]
tool_args: Dict[str, Any] = {"top_k": 5}
if isinstance(first_image, str) and first_image.startswith("data:"):
tool_args["image_data"] = first_image
elif isinstance(first_image, str):
tool_args["image_url"] = first_image
try:
tool_res = await tool_manager.execute_tool(
"nature_id_identify",
tool_args,
agent_id=request_agent_id,
chat_id=chat_id,
user_id=user_id,
)
if tool_res and tool_res.success and tool_res.result:
plant_vision = _parse_tool_json_result(tool_res.result)
if plant_vision:
top_k_rows = plant_vision.get("top_k") or []
top1 = top_k_rows[0] if top_k_rows else {}
confidence = float(plant_vision.get("confidence") or top1.get("confidence") or 0.0)
effective_metadata["plant_vision"] = plant_vision
effective_metadata["plant_top_k"] = top_k_rows
effective_metadata["plant_confidence"] = confidence
request.prompt = (
f"{request.prompt}\n\n"
f"[PLANT_VISION_PREPROCESSED] {json.dumps(plant_vision, ensure_ascii=False)}"
)
if top1:
logger.info(
f"🌿 Vision pre-process: {confidence:.2f}% {top1.get('scientific_name') or top1.get('name') or 'unknown'}"
)
else:
logger.info("🌿 Vision pre-process: no candidates")
if plant_vision.get("recommend_fallback"):
logger.info("🌿 Vision pre-process: low confidence -> GBIF fallback enabled")
# Persist structured plant result for follow-up questions.
if http_client and user_id and chat_id:
fact_key = f"last_plant:{request_agent_id}:{chat_id}"
try:
await http_client.post(
"http://memory-service:8000/facts/upsert",
json={
"user_id": user_id,
"fact_key": fact_key,
"fact_value": (top1.get("scientific_name") if isinstance(top1, dict) else None),
"fact_value_json": {
"top_k": top_k_rows,
"confidence": confidence,
"recommend_fallback": bool(plant_vision.get("recommend_fallback")),
"gbif_validation": plant_vision.get("gbif_validation"),
"identified_at": datetime.utcnow().isoformat(),
"agent_id": request_agent_id,
"chat_id": chat_id,
"source": "plant_vision_preprocess",
},
},
timeout=8.0,
)
except Exception as e:
logger.warning(f"⚠️ Failed to store last_plant fact: {e}")
except Exception as e:
logger.warning(f"⚠️ Plant pre-vision failed: {e}")
# =========================================================================
# CREWAI DECISION: Use orchestration or direct LLM?
# =========================================================================
@@ -1587,10 +1459,6 @@ async def agent_infer(agent_id: str, request: InferRequest):
},
"metadata": effective_metadata,
"runtime_envelope": runtime_envelope,
"plant_vision": effective_metadata.get("plant_vision"),
"plant_top_k": effective_metadata.get("plant_top_k"),
"plant_confidence": effective_metadata.get("plant_confidence"),
"last_plant": effective_metadata.get("last_plant"),
},
team=crewai_cfg.get("team"),
profile=effective_metadata.get("crewai_profile")