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>
This commit is contained in:
Apple
2026-02-09 08:46:46 -08:00
parent 134c044c21
commit ef3473db21
9473 changed files with 408933 additions and 2769877 deletions

View File

@@ -0,0 +1 @@
# AgroMatrix Crew

View File

@@ -0,0 +1,16 @@
from crewai import Agent
from crews.agromatrix_crew import tools
def build_iot():
return Agent(
role="IoT Agent",
goal="Читати телеметрію ThingsBoard і публікувати події в NATS.",
backstory="Доступ лише через ThingsBoard/NATS інструменти.",
tools=[
tools.tool_thingsboard_read,
tools.tool_event_bus
],
allow_delegation=False,
verbose=True
)

View File

@@ -0,0 +1,16 @@
from crewai import Agent
from crews.agromatrix_crew import tools
def build_operations():
return Agent(
role="Operations Agent",
goal="Операційні дії по farmOS (читання/через integration write).",
backstory="Ти працюєш з farmOS лише через інструменти. Прямі записи заборонені.",
tools=[
tools.tool_farmos_read,
tools.tool_integration_write
],
allow_delegation=False,
verbose=True
)

View File

@@ -0,0 +1,16 @@
from crewai import Agent
from crews.agromatrix_crew import tools
def build_platform():
return Agent(
role="Platform Agent",
goal="Платформна перевірка стану сервісів/інтеграцій.",
backstory="Доступ лише через інструменти подій/читання.",
tools=[
tools.tool_event_bus,
tools.tool_farmos_read
],
allow_delegation=False,
verbose=True
)

View File

@@ -0,0 +1,15 @@
from crewai import Agent
from crews.agromatrix_crew import tools
def build_spreadsheet():
return Agent(
role="Spreadsheet Agent",
goal="Читати/редагувати/створювати XLSX файли та формувати артефакти.",
backstory="Використовує лише spreadsheet інструмент.",
tools=[
tools.tool_spreadsheet
],
allow_delegation=False,
verbose=True
)

View File

@@ -0,0 +1,11 @@
from crewai import Agent
def build_stepan():
return Agent(
role="Stepan (AgroMatrix Orchestrator)",
goal="Керувати запитами користувача через делегування під-агентам і повертати єдину відповідь.",
backstory="Ти єдиний канал спілкування з користувачем. Під-агенти працюють лише через інструменти.",
allow_delegation=True,
verbose=True
)

View File

@@ -0,0 +1,15 @@
from crewai import Agent
from crews.agromatrix_crew import tools
def build_sustainability():
return Agent(
role="Sustainability Agent",
goal="Агрегати та аналітика (LiteFarm read-only).",
backstory="Працює лише з read-only LiteFarm інструментом.",
tools=[
tools.tool_litefarm_read
],
allow_delegation=False,
verbose=True
)

View File

@@ -0,0 +1,41 @@
import json
import os
import time
import uuid
import hashlib
from pathlib import Path
from nats.aio.client import Client as NATS
import asyncio
NATS_URL = os.getenv('NATS_URL', 'nats://localhost:4222')
AUDIT_FILE = os.getenv('AGX_AUDIT_FILE', 'artifacts/audit.log.jsonl')
def _hash(text: str):
return hashlib.sha256(text.encode()).hexdigest()[:16]
async def _publish_nats(subject: str, payload: dict):
nc = NATS()
await nc.connect(servers=[NATS_URL])
await nc.publish(subject, json.dumps(payload).encode())
await nc.flush(1)
await nc.drain()
def audit_event(event: dict):
Path(AUDIT_FILE).parent.mkdir(parents=True, exist_ok=True)
with open(AUDIT_FILE, 'a', encoding='utf-8') as f:
f.write(json.dumps(event, ensure_ascii=False) + '\n')
try:
asyncio.run(_publish_nats('agx.audit.delegation', event))
except Exception:
pass
def new_trace(user_request: str):
return {
'trace_id': str(uuid.uuid4()),
'user_request_hash': _hash(user_request),
'ts': int(time.time() * 1000)
}

View File

@@ -0,0 +1,85 @@
{
"OperationPlan": {
"plan_id": "string",
"created_ts": "ISO8601",
"updated_ts": "ISO8601",
"trace_id": "string",
"source": "telegram|excel|api",
"status": "planned|scheduled|in_progress|done|verified|closed|cancelled",
"scope": {
"field_ids": [
"field_001"
],
"crop_ids": [
"crop_wheat_winter"
],
"date_window": {
"start": "YYYY-MM-DD",
"end": "YYYY-MM-DD"
}
},
"tasks": [
{
"task_id": "task_xxx",
"operation_id": "op_sowing",
"planned_date": "YYYY-MM-DD",
"priority": "low|normal|high|critical",
"assignee": "string",
"norms": {
"labor_hours": 0.0,
"fuel_l": 0.0,
"materials": [
{
"material_id": "mat_urea",
"rate": {
"value": 150,
"unit": "kg/ha"
}
}
]
},
"constraints": {
"weather": [
"no_rain"
],
"window": {
"start": "YYYY-MM-DD",
"end": "YYYY-MM-DD"
}
},
"notes": ""
}
],
"fact_events": [
{
"fact_id": "fact_xxx",
"task_id": "task_xxx",
"ts": "ISO8601",
"field_id": "field_001",
"operation_id": "op_sowing",
"done_date": "YYYY-MM-DD",
"fact": {
"labor_hours": 0.0,
"fuel_l": 0.0,
"materials": [
{
"material_id": "mat_urea",
"amount": {
"value": 180,
"unit": "kg/ha"
}
}
]
},
"quality": {
"source": "manual|sensor|import",
"confidence": "trusted|low_confidence"
},
"farmos_write": {
"status": "pending|ok|failed",
"ref": ""
}
}
]
}
}

View File

@@ -0,0 +1,364 @@
import os
import re
import shlex
from agromatrix_tools import tool_dictionary_review as review
CATEGORIES = {"field","crop","operation","material","unit"}
def is_operator(user_id: str | None, chat_id: str | None) -> bool:
allowed_ids = [s.strip() for s in os.getenv('AGX_OPERATOR_IDS', '').split(',') if s.strip()]
allowed_chat = os.getenv('AGX_OPERATOR_CHAT_ID', '').strip()
if allowed_chat and chat_id and str(chat_id) != allowed_chat:
return False
if allowed_ids and user_id and str(user_id) in allowed_ids:
return True
return False
def parse_operator_command(text: str):
parts = shlex.split(text)
if not parts:
return None
cmd = parts[0].lstrip('/')
args = parts[1:]
return {"cmd": cmd, "args": args}
def _wrap(summary: str, details: dict | None = None):
return {
"status": "ok",
"summary": summary,
"artifacts": [],
"tool_calls": [],
"next_actions": [],
"details": details or {}
}
def _extract_ref(text: str) -> str | None:
m = re.search(r"pending\.jsonl:\d+", text)
return m.group(0) if m else None
def _extract_index(text: str) -> int | None:
m = re.search(r"(\d{1,3})", text)
if not m:
return None
try:
return int(m.group(1))
except Exception:
return None
def _extract_limit(text: str, default: int = 10) -> int:
m = re.search(r"(?:до|limit)\s*(\d{1,3})", text)
if m:
try:
return int(m.group(1))
except Exception:
return default
m = re.search(r"(\d{1,3})", text)
if m:
try:
return int(m.group(1))
except Exception:
return default
return default
def _extract_category(text: str) -> str | None:
t = text.lower()
if 'один' in t or 'unit' in t:
return 'unit'
if 'операц' in t or 'operation' in t:
return 'operation'
if 'культур' in t or 'crop' in t:
return 'crop'
if 'матеріал' in t or 'material' in t:
return 'material'
if 'поле' in t or 'field' in t:
return 'field'
return None
def _extract_canonical_id(text: str) -> str | None:
m = re.search(r"(?:як|as)\s+([\w\-:.]+)", text, flags=re.IGNORECASE)
if m:
return m.group(1)
return None
def _extract_reason(text: str) -> str:
if ':' in text:
return text.split(':', 1)[1].strip()
m = re.search(r"(?:бо|через)\s+(.+)$", text, flags=re.IGNORECASE)
if m:
return m.group(1).strip()
return ''
def _resolve_ref_by_index(last_list: list | None, idx: int | None) -> str | None:
if not last_list or not idx:
return None
if idx < 1 or idx > len(last_list):
return None
item = last_list[idx - 1]
return item.get('pending_ref') or item.get('ref')
def handle_whoami(user_id: str | None, chat_id: str | None):
summary = "user_id: {}\nchat_id: {}".format(user_id or '', chat_id or '')
return _wrap(summary)
def handle_pending(limit=10, category=None):
items = review.list_pending(limit=limit, category=category)
lines = []
pending_items = []
for item in items:
sug = item.get('suggestions', [])[:5]
sug_s = ', '.join([f"{s['id']}({s['score']:.2f})" for s in sug])
lines.append(f"{item['pending_ref']} | {item.get('category')} | {item.get('raw_term')} | {sug_s}")
pending_items.append({
'pending_ref': item.get('pending_ref'),
'pending_id': item.get('pending_id'),
'raw_term': item.get('raw_term'),
'category': item.get('category'),
'suggestions': [{"id": s.get('id'), "score": s.get('score')} for s in sug]
})
summary = "\n".join(lines) if lines else "Немає pending записів."
return _wrap(summary, {"count": len(items), "pending_items": pending_items})
def handle_pending_show(ref: str):
detail = review.get_pending_detail(ref)
if not detail:
return _wrap('Не знайдено pending запис')
lines = [
f"ref: {detail.get('ref')}",
f"category: {detail.get('category')}",
f"raw_term: {detail.get('raw_term')}",
f"ts: {detail.get('ts')}",
f"status: {detail.get('status')}"
]
if detail.get('decision'):
lines.append(f"decision: {detail.get('decision')}")
if detail.get('reason'):
lines.append(f"reason: {detail.get('reason')}")
lines.append('suggestions:')
suggestions = detail.get('suggestions') or []
if suggestions:
for s in suggestions:
score = s.get('score')
score_s = f"{score:.2f}" if isinstance(score, (int, float)) else "n/a"
lines.append(f"- {s.get('id')} ({score_s})")
else:
lines.append('- (none)')
return _wrap("\n".join(lines), detail)
def handle_approve(ref, map_to=None, create_new=None, category=None, name=None, id_=None, apply=False):
if map_to:
action = {"type": "map_to_existing", "canonical_id": map_to}
elif create_new:
action = {"type": "create_new_entry", "canonical_id": id_ or '', "canonical_name": name or ''}
else:
action = {"type": "add_synonym", "canonical_id": map_to}
res = review.approve_pending(ref, action)
if apply:
review.apply_resolutions()
return _wrap(f"approved {ref}", {"record": res, "applied": apply})
def handle_reject(ref, reason):
res = review.reject_pending(ref, reason)
return _wrap(f"rejected {ref}", {"record": res})
def handle_apply():
count = review.apply_resolutions()
return _wrap(f"applied {count}")
def handle_stats():
stats = review.stats()
return _wrap(f"open={stats['open']} approved={stats['approved']} rejected={stats['rejected']}", stats)
def route_operator_text(text: str, user_id: str | None, chat_id: str | None, last_pending_list: list | None = None):
if not is_operator(user_id, chat_id):
return {
"status": "error",
"summary": "Недостатньо прав",
"artifacts": [],
"tool_calls": [],
"next_actions": []
}
t = text.strip().lower()
# list pending
if any(k in t for k in ['покажи', 'показати', 'невпізнан', 'непізнан', 'pending', 'невідом']):
category = _extract_category(t)
limit = _extract_limit(t, default=10)
if limit < 1:
limit = 1
if limit > 100:
limit = 100
if category and category not in CATEGORIES:
return _wrap('unknown category')
return handle_pending(limit=limit, category=category)
# stats
if any(k in t for k in ['статист', 'скільки pending', 'pending stats']):
return handle_stats()
# apply
if any(k in t for k in ['застосуй', 'apply', 'застосувати', 'застосуй зміни']):
return handle_apply()
# details
if any(k in t for k in ['деталі', 'detail', 'подробиц', 'покажи деталі']):
ref = _extract_ref(t)
if not ref:
idx = _extract_index(t)
ref = _resolve_ref_by_index(last_pending_list, idx)
if not ref:
return _wrap('Немає ref або контексту для деталей')
return handle_pending_show(ref)
# approve
if any(k in t for k in ['підтверд', 'approve', 'схвали']):
ref = _extract_ref(t)
if not ref:
idx = _extract_index(t)
ref = _resolve_ref_by_index(last_pending_list, idx)
if not ref:
return _wrap('Немає ref або контексту для підтвердження')
canonical_id = _extract_canonical_id(t)
if not canonical_id:
return _wrap('Вкажіть canonical_id після "як"')
return handle_approve(ref, map_to=canonical_id)
# reject
if any(k in t for k in ['відхил', 'reject', 'забракуй']):
ref = _extract_ref(t)
if not ref:
idx = _extract_index(t)
ref = _resolve_ref_by_index(last_pending_list, idx)
if not ref:
return _wrap('Немає ref або контексту для відхилення')
reason = _extract_reason(text)
if not reason:
return _wrap('Вкажіть причину відхилення')
return handle_reject(ref, reason)
return None
def route_operator_command(text: str, user_id: str | None, chat_id: str | None):
parsed = parse_operator_command(text)
if not parsed:
return None
if not is_operator(user_id, chat_id):
return {
"status": "error",
"summary": "Недостатньо прав",
"artifacts": [],
"tool_calls": [],
"next_actions": []
}
cmd = parsed['cmd']
args = parsed['args']
if cmd == 'whoami':
return handle_whoami(user_id, chat_id)
if cmd == 'pending_show':
if not args:
return _wrap('Потрібен ref')
return handle_pending_show(args[0])
if cmd == 'pending':
limit = 10
category = None
i = 0
while i < len(args):
if args[i] == '--limit' and i + 1 < len(args):
try:
limit = int(args[i+1])
except Exception:
limit = 10
i += 2
continue
if args[i] == '--category' and i + 1 < len(args):
category = args[i+1]
i += 2
continue
i += 1
# clamp
if limit < 1:
limit = 1
if limit > 100:
limit = 100
if category and category not in CATEGORIES:
return _wrap('unknown category')
return handle_pending(limit=limit, category=category)
if cmd == 'approve':
if not args:
return _wrap('missing ref')
ref = args[0]
map_to = None
create_new = False
category = None
name = None
id_ = None
apply = False
i = 1
while i < len(args):
if args[i] == 'map_to':
map_to = args[i+1]
i += 2
elif args[i] == '--apply':
apply = True
i += 1
elif args[i] == 'create_new':
create_new = True
i += 1
elif args[i] == 'category':
category = args[i+1]
i += 2
elif args[i] == 'name':
name = args[i+1]
i += 2
elif args[i] == 'id':
id_ = args[i+1]
i += 2
else:
i += 1
if apply:
allow_apply = os.getenv('AGX_ALLOW_APPLY', '0') == '1' or os.getenv('AGX_OPS_MODE', '0') == '1'
if not allow_apply:
return _wrap('apply_not_allowed')
return handle_approve(ref, map_to=map_to, create_new=create_new, category=category, name=name, id_=id_, apply=apply)
if cmd == 'reject':
if len(args) < 2:
return _wrap('missing ref or reason')
ref = args[0]
reason = ' '.join(args[1:])
return handle_reject(ref, reason)
if cmd == 'apply_dict':
return handle_apply()
if cmd == 'pending_stats':
return handle_stats()
return _wrap('unknown command')

View File

@@ -0,0 +1,233 @@
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()

View File

@@ -0,0 +1,7 @@
{
"status": "ok|error",
"summary": "string",
"artifacts": [{"type": "string", "path": "string", "description": "string"}],
"tool_calls": [{"tool": "string", "input": {}, "output_ref": "string"}],
"next_actions": ["string"]
}

View File

@@ -0,0 +1,9 @@
from crewai import Task
def build_task(agent, instructions: str):
return Task(
description=f"Виконай IoT/ThingsBoard дії: {instructions}",
expected_output="JSON строго за схемою у crews/agromatrix_crew/schema.json. "Структурований результат IoT дій.",
agent=agent
)

View File

@@ -0,0 +1,9 @@
from crewai import Task
def build_task(agent, instructions: str):
return Task(
description=f"Виконай операційні дії: {instructions}",
expected_output="JSON строго за схемою у crews/agromatrix_crew/schema.json. "Структурований результат виконання операцій.",
agent=agent
)

View File

@@ -0,0 +1,9 @@
from crewai import Task
def build_task(agent, instructions: str):
return Task(
description=f"Виконай дії з таблицями: {instructions}",
expected_output="JSON строго за схемою у crews/agromatrix_crew/schema.json. "Посилання на артефакти та короткий опис змін.",
agent=agent
)

View File

@@ -0,0 +1,9 @@
from crewai import Task
def build_task(stepan, user_request: str):
return Task(
description=f"Оціни запит користувача і склади план делегування: {user_request}",
expected_output="JSON строго за схемою у crews/agromatrix_crew/schema.json. "Структурований план: які агенти, які задачі, які інструменти.",
agent=stepan
)

View File

@@ -0,0 +1,9 @@
from crewai import Task
def build_task(stepan, summary: str):
return Task(
description=f"Сформуй фінальну відповідь користувачу на основі: {summary}",
expected_output="JSON строго за схемою у crews/agromatrix_crew/schema.json. "Коротка консолідована відповідь користувачу.",
agent=stepan
)

View File

@@ -0,0 +1,8 @@
from agromatrix_tools import tool_farmos_read
from agromatrix_tools import tool_integration_write
from agromatrix_tools import tool_thingsboard_read
from agromatrix_tools import tool_event_bus
from agromatrix_tools import tool_spreadsheet
from agromatrix_tools import tool_litefarm_read
from agromatrix_tools import tool_tania_control