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>
234 lines
9.2 KiB
Python
234 lines
9.2 KiB
Python
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()
|