Files
microdao-daarion/crews/agromatrix_crew/run.py
Apple ef3473db21 snapshot: NODE1 production state 2026-02-09
Complete snapshot of /opt/microdao-daarion/ from NODE1 (144.76.224.179).
This represents the actual running production code that has diverged
significantly from the previous main branch.

Key changes from old main:
- Gateway (http_api.py): expanded from ~40KB to 164KB with full agent support
- Router: new /v1/agents/{id}/infer endpoint with vision + DeepSeek routing
- Behavior Policy: SOWA v2.2 (3-level: FULL/ACK/SILENT)
- Agent Registry: config/agent_registry.yml as single source of truth
- 13 agents configured (was 3)
- Memory service integration
- CrewAI teams and roles

Excluded from snapshot: venv/, .env, data/, backups, .tgz archives

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 08:46:46 -08:00

234 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
import os
import json
import subprocess
from pathlib import Path
from crewai import Crew, Task
from crews.agromatrix_crew.agents.stepan_orchestrator import build_stepan
from crews.agromatrix_crew.agents.operations_agent import build_operations
from crews.agromatrix_crew.agents.iot_agent import build_iot
from crews.agromatrix_crew.agents.platform_agent import build_platform
from crews.agromatrix_crew.agents.spreadsheet_agent import build_spreadsheet
from crews.agromatrix_crew.agents.sustainability_agent import build_sustainability
from crews.agromatrix_crew.audit import audit_event, new_trace
from agromatrix_tools import tool_dictionary
from agromatrix_tools import tool_operation_plan
from crews.agromatrix_crew.operator_commands import route_operator_command, route_operator_text
def farmos_ui_hint():
port = os.getenv('FARMOS_UI_PORT', '18080')
try:
out = subprocess.check_output(['docker','ps','--format','{{.Names}}'], text=True)
if 'farmos_ui_proxy' in out:
return "\n[UI] farmOS доступний локально: http://127.0.0.1:{} (basic auth)".format(port)
except Exception:
return ""
return ""
def detect_intent(text: str) -> str:
t = text.lower()
if 'сплануй' in t and 'тиж' in t:
return 'plan_week'
if 'сплануй' in t:
return 'plan_day'
if 'критично' in t or 'на завтра' in t:
return 'show_critical_tomorrow'
if 'план/факт' in t or 'план факт' in t:
return 'plan_vs_fact'
if 'закрий план' in t:
return 'close_plan'
return 'general'
def validate_payload(obj: dict):
required = ['status', 'summary', 'artifacts', 'tool_calls', 'next_actions']
for k in required:
if k not in obj:
return False, f'missing:{k}'
if obj['status'] not in ['ok', 'error']:
return False, 'bad_status'
if not isinstance(obj['summary'], str):
return False, 'summary_not_string'
if not isinstance(obj['artifacts'], list):
return False, 'artifacts_not_list'
if not isinstance(obj['tool_calls'], list):
return False, 'tool_calls_not_list'
if not isinstance(obj['next_actions'], list):
return False, 'next_actions_not_list'
return True, 'ok'
def run_task_with_retry(agent, description: str, trace_id: str, max_retries: int = 2):
instruction = "Return ONLY valid JSON matching schema in crews/agromatrix_crew/schema.json. No extra text."
last_error = ''
for attempt in range(max_retries + 1):
desc = description if attempt == 0 else (description + "\n\n" + instruction)
task = Task(
description=desc,
expected_output="JSON strictly matching schema.json",
agent=agent
)
crew = Crew(agents=[agent], tasks=[task], verbose=True)
result = crew.kickoff()
raw = str(result)
try:
data = json.loads(raw)
ok, reason = validate_payload(data)
if ok:
return data
last_error = reason
except Exception as e:
last_error = f'json_error:{e}'
audit_event({
'trace_id': trace_id,
'agent': agent.role,
'action': 'json_validation_failed',
'error': last_error
})
return {
'status': 'error',
'summary': f'JSON validation failed: {last_error}',
'artifacts': [],
'tool_calls': [],
'next_actions': []
}
def handle_message(text: str, user_id: str = '', chat_id: str = '', trace_id: str = '', ops_mode: bool = False, last_pending_list: list | None = None) -> str:
trace = new_trace(text)
if trace_id:
trace['trace_id'] = trace_id
os.environ['AGX_TRACE_ID'] = trace['trace_id']
os.environ['AGX_USER_ID'] = str(user_id)
os.environ['AGX_CHAT_ID'] = str(chat_id)
os.environ['AGX_OPS_MODE'] = '1' if ops_mode else '0'
# operator commands
if text.strip().startswith('/'):
op_res = route_operator_command(text, str(user_id), str(chat_id))
if op_res:
return json.dumps(op_res, ensure_ascii=False)
elif ops_mode:
op_res = route_operator_text(text, str(user_id), str(chat_id), last_pending_list=last_pending_list)
if op_res:
return json.dumps(op_res, ensure_ascii=False)
stepan = build_stepan()
ops = build_operations()
iot = build_iot()
platform = build_platform()
spreadsheet = build_spreadsheet()
sustainability = build_sustainability()
audit_event({**trace, 'agent': 'stepan', 'action': 'intake'})
# Preflight normalization
norm = tool_dictionary.normalize_from_text(text, trace_id=trace['trace_id'], source='telegram')
pending = [item for cat in norm.values() for item in cat if item.get('status') == 'pending']
if pending:
lines = ["=== PENDING TERMS (Stepan) ==="]
for item in pending:
lines.append(f"- {item.get('term')}: {item.get('suggestions', [])[:3]}")
lines.append("\nБудь ласка, уточніть невідомі терміни. Після підтвердження я продовжу.")
return "\n".join(lines)
intent = detect_intent(text)
pending_count = 0
if ops_mode:
try:
from agromatrix_tools import tool_dictionary_review as review
pending_count = review.stats().get('open', 0)
except Exception:
pending_count = 0
if intent in ['plan_week', 'plan_day']:
plan_id = tool_operation_plan.create_plan({
'scope': {
'field_ids': [i.get('normalized_id') for i in norm.get('fields', []) if i.get('status')=='ok'],
'crop_ids': [i.get('normalized_id') for i in norm.get('crops', []) if i.get('status')=='ok'],
'date_window': {'start': '', 'end': ''}
},
'tasks': []
}, trace_id=trace['trace_id'], source='telegram')
return json.dumps({
'status': 'ok',
'summary': f'План створено: {plan_id}',
'artifacts': [],
'tool_calls': [],
'next_actions': ['уточнити дати та операції'],
'pending_dictionary_items': pending_count if ops_mode else None
}, ensure_ascii=False)
if intent == 'show_critical_tomorrow':
_ = tool_operation_plan.plan_dashboard({}, {})
return json.dumps({
'status': 'ok',
'summary': 'Критичні задачі на завтра',
'artifacts': [],
'tool_calls': [],
'next_actions': [],
'pending_dictionary_items': pending_count if ops_mode else None
}, ensure_ascii=False)
if intent == 'plan_vs_fact':
_ = tool_operation_plan.plan_dashboard({}, {})
return json.dumps({
'status': 'ok',
'summary': 'План/факт зведення',
'artifacts': [],
'tool_calls': [],
'next_actions': [],
'pending_dictionary_items': pending_count if ops_mode else None
}, ensure_ascii=False)
# general crew flow
ops_out = run_task_with_retry(ops, "Оціни чи потрібні операційні записи або читання farmOS", trace['trace_id'])
iot_out = run_task_with_retry(iot, "Оціни чи є потреба в даних ThingsBoard або NATS", trace['trace_id'])
platform_out = run_task_with_retry(platform, "Перевір базовий статус сервісів/інтеграцій", trace['trace_id'])
sheet_out = run_task_with_retry(spreadsheet, "Якщо запит стосується таблиць — підготуй артефакти", trace['trace_id'])
sustainability_out = run_task_with_retry(sustainability, "Якщо потрібні агрегації — дай read-only підсумки", trace['trace_id'])
audit_event({**trace, 'agent': 'stepan', 'action': 'delegate', 'targets': ['ops','iot','platform','spreadsheet','sustainability']})
summary = {
'ops': ops_out,
'iot': iot_out,
'platform': platform_out,
'spreadsheet': sheet_out,
'sustainability': sustainability_out
}
final_task = Task(
description=f"Сформуй фінальну коротку відповідь користувачу. Вхідні дані (JSON): {json.dumps(summary, ensure_ascii=False)}",
expected_output="Коротка консолідована відповідь для користувача українською.",
agent=stepan
)
crew = Crew(agents=[stepan], tasks=[final_task], verbose=True)
result = crew.kickoff()
return str(result) + farmos_ui_hint()
def main():
if len(sys.argv) < 2:
print('Usage: python run.py [--trace <id>] "<user request>"')
sys.exit(1)
args = sys.argv[1:]
trace_override = None
if args and args[0] == '--trace':
trace_override = args[1]
args = args[2:]
user_request = args[0]
output = handle_message(user_request, user_id=os.getenv('AGX_USER_ID',''), chat_id=os.getenv('AGX_CHAT_ID',''), trace_id=trace_override or '', ops_mode=os.getenv('AGX_OPS_MODE','0')=='1')
print(output)
if __name__ == '__main__':
main()