runtime: sync router/gateway/config policy and clan role registry
This commit is contained in:
@@ -72,6 +72,23 @@ AGENT_SPECIALIZED_TOOLS = {
|
||||
# SenpAI (Gordon Senpai) - Trading & Markets
|
||||
# Specialized: real-time market data, features, signals
|
||||
"senpai": ['market_data', 'comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# 1OK - Window Master Assistant
|
||||
# Specialized: CRM flow, quoting, PDF docs, scheduling
|
||||
"oneok": [
|
||||
"crm_search_client",
|
||||
"crm_upsert_client",
|
||||
"crm_upsert_site",
|
||||
"crm_upsert_window_unit",
|
||||
"crm_create_quote",
|
||||
"crm_update_quote",
|
||||
"crm_create_job",
|
||||
"calc_window_quote",
|
||||
"docs_render_quote_pdf",
|
||||
"docs_render_invoice_pdf",
|
||||
"schedule_propose_slots",
|
||||
"schedule_confirm_slot",
|
||||
],
|
||||
|
||||
# Soul / Athena - Spiritual Mentor
|
||||
"soul": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
@@ -363,6 +363,8 @@ class MemoryRetrieval:
|
||||
query: str,
|
||||
agent_id: str = "helion",
|
||||
platform_user_id: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
visibility: str = "platform",
|
||||
limit: int = 5
|
||||
) -> List[Dict[str, Any]]:
|
||||
@@ -377,6 +379,16 @@ class MemoryRetrieval:
|
||||
|
||||
all_results = []
|
||||
|
||||
q = (query or "").lower()
|
||||
# If user explicitly asks about documents/catalogs, prefer knowledge base docs over chat snippets.
|
||||
is_doc_query = any(k in q for k in ["pdf", "каталог", "каталоз", "документ", "файл", "стор", "page", "pages"])
|
||||
# Simple keyword gate to avoid irrelevant chat snippets dominating doc queries.
|
||||
# Example: when asking "з каталогу Defenda 2026 ... гліфосат", old "Бокаші" messages may match too well.
|
||||
topic_keywords: List[str] = []
|
||||
for kw in ["defenda", "ifagri", "bayer", "гліфосат", "glyphos", "глифос", "npk", "мінерал", "добрив", "гербіц", "фунгіц", "інсектиц"]:
|
||||
if kw in q:
|
||||
topic_keywords.append(kw)
|
||||
|
||||
# Dynamic collection names based on agent_id
|
||||
memory_items_collection = f"{agent_id}_memory_items"
|
||||
messages_collection = f"{agent_id}_messages"
|
||||
@@ -420,18 +432,34 @@ class MemoryRetrieval:
|
||||
|
||||
# Search 2: {agent_id}_messages (chat history)
|
||||
try:
|
||||
msg_filter = None
|
||||
if chat_id:
|
||||
# Payload schema differs across ingesters: some use chat_id, others channel_id.
|
||||
msg_filter = qmodels.Filter(
|
||||
should=[
|
||||
qmodels.FieldCondition(key="chat_id", match=qmodels.MatchValue(value=str(chat_id))),
|
||||
qmodels.FieldCondition(key="channel_id", match=qmodels.MatchValue(value=str(chat_id))),
|
||||
]
|
||||
)
|
||||
results = self.qdrant_client.search(
|
||||
collection_name=messages_collection,
|
||||
query_vector=embedding,
|
||||
query_filter=msg_filter,
|
||||
limit=limit,
|
||||
with_payload=True
|
||||
)
|
||||
|
||||
for r in results:
|
||||
if r.score > 0.4: # Higher threshold for messages
|
||||
# Higher threshold for messages; even higher when user asks about docs to avoid pulling old chatter.
|
||||
msg_thresh = 0.5 if is_doc_query else 0.4
|
||||
if r.score > msg_thresh:
|
||||
text = r.payload.get("text", r.payload.get("content", ""))
|
||||
# Skip very short or system messages
|
||||
if len(text) > 20 and not text.startswith("<"):
|
||||
if is_doc_query and topic_keywords:
|
||||
tl = text.lower()
|
||||
if not any(k in tl for k in topic_keywords):
|
||||
continue
|
||||
all_results.append({
|
||||
"text": text,
|
||||
"type": "message",
|
||||
@@ -446,18 +474,21 @@ class MemoryRetrieval:
|
||||
results = self.qdrant_client.search(
|
||||
collection_name=docs_collection,
|
||||
query_vector=embedding,
|
||||
limit=3, # Less docs, they're usually longer
|
||||
limit=6 if is_doc_query else 3, # Pull more docs for explicit doc queries
|
||||
with_payload=True
|
||||
)
|
||||
|
||||
for r in results:
|
||||
if r.score > 0.5: # Higher threshold for docs
|
||||
# When user asks about PDF/catalogs, relax threshold so docs show up more reliably.
|
||||
doc_thresh = 0.35 if is_doc_query else 0.5
|
||||
if r.score > doc_thresh:
|
||||
text = r.payload.get("text", r.payload.get("content", ""))
|
||||
if len(text) > 30:
|
||||
all_results.append({
|
||||
"text": text[:500], # Truncate long docs
|
||||
"type": "knowledge",
|
||||
"score": r.score,
|
||||
# Slightly boost docs for doc queries so they win vs chat snippets.
|
||||
"score": (r.score + 0.12) if is_doc_query else r.score,
|
||||
"source": "docs"
|
||||
})
|
||||
except Exception as e:
|
||||
@@ -614,7 +645,8 @@ class MemoryRetrieval:
|
||||
message_text: str,
|
||||
response_text: str,
|
||||
chat_id: str,
|
||||
message_type: str = "conversation"
|
||||
message_type: str = "conversation",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Store a message exchange in agent-specific Qdrant collection.
|
||||
@@ -656,23 +688,27 @@ class MemoryRetrieval:
|
||||
|
||||
# Store in Qdrant
|
||||
point_id = str(uuid.uuid4())
|
||||
payload = {
|
||||
"text": combined_text[:5000], # Limit payload size
|
||||
"user_message": message_text[:2000],
|
||||
"assistant_response": response_text[:3000],
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"chat_id": chat_id,
|
||||
"agent_id": agent_id,
|
||||
"type": message_type,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
if metadata and isinstance(metadata, dict):
|
||||
payload["metadata"] = metadata
|
||||
|
||||
self.qdrant_client.upsert(
|
||||
collection_name=messages_collection,
|
||||
points=[
|
||||
qmodels.PointStruct(
|
||||
id=point_id,
|
||||
vector=embedding,
|
||||
payload={
|
||||
"text": combined_text[:5000], # Limit payload size
|
||||
"user_message": message_text[:2000],
|
||||
"assistant_response": response_text[:3000],
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"chat_id": chat_id,
|
||||
"agent_id": agent_id,
|
||||
"type": message_type,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
payload=payload
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -111,6 +111,16 @@ llm_profiles:
|
||||
timeout_ms: 60000
|
||||
description: "Mistral Large для складних задач, reasoning, аналізу"
|
||||
|
||||
cloud_grok:
|
||||
provider: grok
|
||||
base_url: https://api.x.ai
|
||||
api_key_env: GROK_API_KEY
|
||||
model: grok-2-1212
|
||||
max_tokens: 2048
|
||||
temperature: 0.2
|
||||
timeout_ms: 60000
|
||||
description: "Grok для SOFIIA (технічний суверен)"
|
||||
|
||||
# ============================================================================
|
||||
# Orchestrator Providers
|
||||
# ============================================================================
|
||||
@@ -132,7 +142,7 @@ orchestrator_providers:
|
||||
agents:
|
||||
devtools:
|
||||
description: "DevTools Agent - помічник з кодом, тестами й інфраструктурою"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: local_qwen3_8b
|
||||
system_prompt: |
|
||||
Ти - DevTools Agent в екосистемі DAARION.city.
|
||||
Ти допомагаєш розробникам з:
|
||||
@@ -161,7 +171,7 @@ agents:
|
||||
|
||||
microdao_orchestrator:
|
||||
description: "Multi-agent orchestrator for MicroDAO workflows"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_strategist_8b
|
||||
system_prompt: |
|
||||
You are the central router/orchestrator for DAARION.city MicroDAO.
|
||||
Coordinate multiple agents, respect RBAC, escalate only when needed.
|
||||
@@ -169,7 +179,7 @@ agents:
|
||||
|
||||
daarwizz:
|
||||
description: "DAARWIZZ — головний оркестратор DAARION Core"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_strategist_8b
|
||||
system_prompt: |
|
||||
Ти — DAARWIZZ, головний стратег MicroDAO DAARION.city.
|
||||
Тримаєш контекст roadmap, delegation, crew-команд.
|
||||
@@ -178,7 +188,7 @@ agents:
|
||||
|
||||
greenfood:
|
||||
description: "GREENFOOD Assistant - ERP orchestrator"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: mistral_community_7b
|
||||
system_prompt: |
|
||||
Ти — GREENFOOD Assistant, фронтовий оркестратор ERP-системи для крафтових виробників.
|
||||
Розумій, хто з тобою говорить (комітент, покупець, склад, бухгалтер), та делегуй задачі відповідним під-агентам.
|
||||
@@ -203,21 +213,21 @@ agents:
|
||||
|
||||
agromatrix:
|
||||
description: "AgroMatrix — агроаналітика та кооперація"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_science_8b
|
||||
system_prompt: |
|
||||
Ти — AgroMatrix, AI-агент для агроаналітики, планування сезонів та кооперації фермерів.
|
||||
Відповідай лаконічно, давай практичні поради для агросектору.
|
||||
|
||||
alateya:
|
||||
description: "Alateya — R&D та біотех інновації"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_science_8b
|
||||
system_prompt: |
|
||||
Ти — Alateya, AI-агент для R&D, біотеху та інноваційних досліджень.
|
||||
Відповідай точними, структурованими відповідями та посилайся на джерела, якщо є.
|
||||
|
||||
clan:
|
||||
description: "CLAN — комунікації кооперативів"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: mistral_community_7b
|
||||
system_prompt: |
|
||||
Ти — CLAN, координуєш комунікацію, оголошення та community operations.
|
||||
Відповідай лише коли тема стосується координації, а звернення адресовано тобі (тег @ClanBot чи згадка кланів).
|
||||
@@ -225,7 +235,7 @@ agents:
|
||||
|
||||
soul:
|
||||
description: "SOUL / Spirit — духовний гід комʼюніті"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: mistral_community_7b
|
||||
system_prompt: |
|
||||
Ти — Spirit/SOUL, ментор живої операційної системи.
|
||||
Пояснюй місію, підтримуй мораль, працюй із soft-skills.
|
||||
@@ -233,7 +243,7 @@ agents:
|
||||
|
||||
druid:
|
||||
description: "DRUID — R&D агент з косметології та eco design"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_science_8b
|
||||
system_prompt: |
|
||||
Ти — DRUID AI, експерт з космецевтики, біохімії та сталого дизайну.
|
||||
Працюй з формулами, стехіометрією, етичними ланцюгами постачання.
|
||||
@@ -269,7 +279,7 @@ agents:
|
||||
|
||||
nutra:
|
||||
description: "NUTRA — нутріцевтичний агент"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_science_8b
|
||||
system_prompt: |
|
||||
Ти — NUTRA, допомагаєш з формулами нутрієнтів, біомедичних добавок та лабораторних інтерпретацій.
|
||||
Відповідай з науковою точністю, посилайся на джерела, якщо можливо.
|
||||
@@ -298,7 +308,7 @@ agents:
|
||||
|
||||
eonarch:
|
||||
description: "EONARCH — мультимодальний агент (vision + chat)"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: mistral_community_7b
|
||||
system_prompt: |
|
||||
Ти — EONARCH, аналізуєш зображення, PDF та текстові запити.
|
||||
Враховуй присутність інших ботів та працюй лише за прямим тегом або коли потрібно мультимодальне тлумачення.
|
||||
@@ -318,7 +328,7 @@ agents:
|
||||
|
||||
helion:
|
||||
description: "Helion - AI agent for Energy Union platform"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_science_8b
|
||||
system_prompt: |
|
||||
Ти - Helion, AI-агент платформи Energy Union.
|
||||
Допомагай користувачам з технологіями EcoMiner/BioMiner, токеномікою та DAO governance.
|
||||
@@ -399,7 +409,7 @@ agents:
|
||||
|
||||
yaromir:
|
||||
description: "Yaromir CrewAI (Вождь/Проводник/Домир/Создатель)"
|
||||
default_llm: cloud_deepseek
|
||||
default_llm: qwen3_strategist_8b
|
||||
system_prompt: |
|
||||
Ти — Yaromir Crew. Стратегія, наставництво, психологічна підтримка команди.
|
||||
Розрізняй інших ботів за ніком та відповідай лише на стратегічні запити.
|
||||
@@ -416,38 +426,18 @@ agents:
|
||||
- id: check_health
|
||||
type: builtin
|
||||
|
||||
|
||||
senpai:
|
||||
description: "SenpAI — Gordon Senpai, trading advisor"
|
||||
description: "SENPAI - Trading Advisor & Capital Markets"
|
||||
default_llm: cloud_deepseek
|
||||
system_prompt: |
|
||||
Ты — Гордон Сэнпай: советник по рынкам капитала и цифровым активам.
|
||||
Помогай мыслить как профессионал: строить систему, управлять риском, оценивать сценарии.
|
||||
tools:
|
||||
- id: web_search
|
||||
type: external
|
||||
endpoint: http://swapper-service:8890/web/search
|
||||
description: "Пошук ринкових даних"
|
||||
- id: web_extract
|
||||
type: external
|
||||
endpoint: http://swapper-service:8890/web/extract
|
||||
description: "Витягнути контент з URL"
|
||||
- id: vision
|
||||
type: llm
|
||||
model: qwen3-vl:8b
|
||||
description: "Аналіз графіків та скріншотів"
|
||||
(loaded from senpai_prompt.txt)
|
||||
|
||||
sofiia:
|
||||
description: "Sofiia — AI assistant for community management"
|
||||
default_llm: cloud_deepseek
|
||||
description: "SOFIIA - Chief AI Architect & Technical Sovereign"
|
||||
default_llm: cloud_grok
|
||||
system_prompt: |
|
||||
Ти — Софія, AI-асистент для управління спільнотою DAARION.
|
||||
Допомагай з організацією, комунікаціями та координацією проектів.
|
||||
tools:
|
||||
- id: web_search
|
||||
type: external
|
||||
endpoint: http://swapper-service:8890/web/search
|
||||
description: "Пошук інформації"
|
||||
(loaded from sofiia_prompt.txt)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Routing Rules
|
||||
@@ -571,7 +561,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: daarwizz
|
||||
use_llm: qwen3_strategist_8b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "Daarwizz orchestrator"
|
||||
|
||||
@@ -579,7 +570,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: greenfood
|
||||
use_llm: mistral_community_7b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "GREENFOOD ERP"
|
||||
|
||||
@@ -587,7 +579,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: agromatrix
|
||||
use_llm: qwen3_science_8b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "AgroMatrix агроаналітика"
|
||||
|
||||
@@ -595,7 +588,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: alateya
|
||||
use_llm: qwen3_science_8b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "Alateya R&D"
|
||||
|
||||
@@ -603,7 +597,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: clan
|
||||
use_llm: mistral_community_7b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "CLAN community operations"
|
||||
|
||||
@@ -611,7 +606,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: soul
|
||||
use_llm: mistral_community_7b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "SOUL / Spirit мотивація"
|
||||
|
||||
@@ -619,7 +615,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: druid
|
||||
use_llm: qwen3_science_8b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "DRUID science"
|
||||
|
||||
@@ -627,7 +624,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: nutra
|
||||
use_llm: qwen3_science_8b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "NUTRA science"
|
||||
|
||||
@@ -635,7 +633,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: eonarch
|
||||
use_llm: mistral_community_7b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "EONARCH vision"
|
||||
|
||||
@@ -652,7 +651,8 @@ routing:
|
||||
priority: 5
|
||||
when:
|
||||
agent: yaromir
|
||||
use_llm: qwen3_strategist_8b
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "Yaromir crew"
|
||||
|
||||
@@ -670,16 +670,27 @@ routing:
|
||||
when:
|
||||
agent: senpai
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "SenpAI trading advisor - DeepSeek"
|
||||
description: "SENPAI trading - DeepSeek"
|
||||
|
||||
- id: sofiia_agent
|
||||
priority: 5
|
||||
when:
|
||||
agent: sofiia
|
||||
use_llm: cloud_deepseek
|
||||
use_llm: cloud_grok
|
||||
fallback_llm: cloud_deepseek
|
||||
use_context_prompt: true
|
||||
description: "Sofiia community assistant - DeepSeek"
|
||||
description: "SOFIIA architect - Grok (fallback DeepSeek)"
|
||||
|
||||
- id: oneok_agent
|
||||
priority: 5
|
||||
when:
|
||||
agent: oneok
|
||||
use_llm: cloud_deepseek
|
||||
fallback_llm: cloud_mistral
|
||||
use_context_prompt: true
|
||||
description: "1OK Window Master - DeepSeek"
|
||||
|
||||
- id: fallback_local
|
||||
priority: 100
|
||||
@@ -705,4 +716,3 @@ policies:
|
||||
enabled: true
|
||||
audit_mode:
|
||||
enabled: false
|
||||
|
||||
|
||||
@@ -449,6 +449,177 @@ TOOL_DEFINITIONS = [
|
||||
"required": ["symbol"]
|
||||
}
|
||||
}
|
||||
},
|
||||
# PRIORITY 8: 1OK Window Master tools
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "crm_search_client",
|
||||
"description": "Пошук клієнта в CRM за телефоном/email/ПІБ.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Телефон, email або ім'я клієнта"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "crm_upsert_client",
|
||||
"description": "Створити або оновити клієнта в CRM.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_payload": {"type": "object", "description": "Дані клієнта"}
|
||||
},
|
||||
"required": ["client_payload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "crm_upsert_site",
|
||||
"description": "Створити або оновити об'єкт (адресу) в CRM.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"site_payload": {"type": "object", "description": "Дані об'єкта/адреси"}
|
||||
},
|
||||
"required": ["site_payload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "crm_upsert_window_unit",
|
||||
"description": "Створити або оновити віконний блок/проріз в CRM.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"window_payload": {"type": "object", "description": "Дані віконного блоку"}
|
||||
},
|
||||
"required": ["window_payload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "crm_create_quote",
|
||||
"description": "Створити quote/КП в CRM.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote_payload": {"type": "object", "description": "Дані КП/розрахунку"}
|
||||
},
|
||||
"required": ["quote_payload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "crm_update_quote",
|
||||
"description": "Оновити існуючий quote в CRM.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote_id": {"type": "string"},
|
||||
"patch": {"type": "object"}
|
||||
},
|
||||
"required": ["quote_id", "patch"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "crm_create_job",
|
||||
"description": "Створити job (замір/монтаж/сервіс) в CRM.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_payload": {"type": "object", "description": "Дані job"}
|
||||
},
|
||||
"required": ["job_payload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calc_window_quote",
|
||||
"description": "Прорахунок вікон через calc-сервіс.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input_payload": {"type": "object", "description": "Вхід для калькулятора"}
|
||||
},
|
||||
"required": ["input_payload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "docs_render_quote_pdf",
|
||||
"description": "Рендер PDF комерційної пропозиції.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote_id": {"type": "string"},
|
||||
"quote_payload": {"type": "object"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "docs_render_invoice_pdf",
|
||||
"description": "Рендер PDF рахунку.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"invoice_payload": {"type": "object", "description": "Дані рахунку"}
|
||||
},
|
||||
"required": ["invoice_payload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "schedule_propose_slots",
|
||||
"description": "Запропонувати слоти на замір/монтаж.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"params": {"type": "object", "description": "Параметри планування"}
|
||||
},
|
||||
"required": ["params"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "schedule_confirm_slot",
|
||||
"description": "Підтвердити обраний слот.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {"type": "string"},
|
||||
"slot": {}
|
||||
},
|
||||
"required": ["job_id", "slot"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -473,6 +644,11 @@ class ToolManager:
|
||||
self.http_client = httpx.AsyncClient(timeout=60.0)
|
||||
self.swapper_url = os.getenv("SWAPPER_URL", "http://swapper-service:8890")
|
||||
self.comfy_agent_url = os.getenv("COMFY_AGENT_URL", "http://212.8.58.133:8880")
|
||||
self.oneok_crm_url = os.getenv("ONEOK_CRM_BASE_URL", "http://oneok-crm-adapter:8088").rstrip("/")
|
||||
self.oneok_calc_url = os.getenv("ONEOK_CALC_BASE_URL", "http://oneok-calc-adapter:8089").rstrip("/")
|
||||
self.oneok_docs_url = os.getenv("ONEOK_DOCS_BASE_URL", "http://oneok-docs-adapter:8090").rstrip("/")
|
||||
self.oneok_schedule_url = os.getenv("ONEOK_SCHEDULE_BASE_URL", "http://oneok-schedule-adapter:8091").rstrip("/")
|
||||
self.oneok_adapter_api_key = os.getenv("ONEOK_ADAPTER_API_KEY", "").strip()
|
||||
self.tools_config = self._load_tools_config()
|
||||
|
||||
def _load_tools_config(self) -> Dict[str, Dict]:
|
||||
@@ -560,6 +736,31 @@ class ToolManager:
|
||||
# Priority 7: Market Data (SenpAI)
|
||||
elif tool_name == "market_data":
|
||||
return await self._market_data(arguments)
|
||||
# Priority 8: 1OK tools
|
||||
elif tool_name == "crm_search_client":
|
||||
return await self._crm_search_client(arguments)
|
||||
elif tool_name == "crm_upsert_client":
|
||||
return await self._crm_upsert_client(arguments)
|
||||
elif tool_name == "crm_upsert_site":
|
||||
return await self._crm_upsert_site(arguments)
|
||||
elif tool_name == "crm_upsert_window_unit":
|
||||
return await self._crm_upsert_window_unit(arguments)
|
||||
elif tool_name == "crm_create_quote":
|
||||
return await self._crm_create_quote(arguments)
|
||||
elif tool_name == "crm_update_quote":
|
||||
return await self._crm_update_quote(arguments)
|
||||
elif tool_name == "crm_create_job":
|
||||
return await self._crm_create_job(arguments)
|
||||
elif tool_name == "calc_window_quote":
|
||||
return await self._calc_window_quote(arguments)
|
||||
elif tool_name == "docs_render_quote_pdf":
|
||||
return await self._docs_render_quote_pdf(arguments)
|
||||
elif tool_name == "docs_render_invoice_pdf":
|
||||
return await self._docs_render_invoice_pdf(arguments)
|
||||
elif tool_name == "schedule_propose_slots":
|
||||
return await self._schedule_propose_slots(arguments)
|
||||
elif tool_name == "schedule_confirm_slot":
|
||||
return await self._schedule_confirm_slot(arguments)
|
||||
else:
|
||||
return ToolResult(success=False, result=None, error=f"Unknown tool: {tool_name}")
|
||||
except Exception as e:
|
||||
@@ -2875,6 +3076,108 @@ class ToolManager:
|
||||
logger.error(f"Market data tool error: {e}")
|
||||
return ToolResult(success=False, result=None, error=str(e))
|
||||
|
||||
async def _oneok_http_call(self, base_url: str, path: str, payload: Dict[str, Any], method: str = "POST") -> ToolResult:
|
||||
url = f"{base_url}{path}"
|
||||
try:
|
||||
method_up = method.upper()
|
||||
headers = {}
|
||||
if self.oneok_adapter_api_key:
|
||||
headers["Authorization"] = f"Bearer {self.oneok_adapter_api_key}"
|
||||
if method_up == "GET":
|
||||
resp = await self.http_client.get(url, params=payload, headers=headers, timeout=30.0)
|
||||
elif method_up == "PATCH":
|
||||
resp = await self.http_client.patch(url, json=payload, headers=headers, timeout=30.0)
|
||||
else:
|
||||
resp = await self.http_client.post(url, json=payload, headers=headers, timeout=30.0)
|
||||
|
||||
if resp.status_code >= 400:
|
||||
body = (resp.text or "")[:500]
|
||||
return ToolResult(success=False, result=None, error=f"{url} -> HTTP {resp.status_code}: {body}")
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
data = {"text": (resp.text or "")[:1000]}
|
||||
return ToolResult(success=True, result=data)
|
||||
except Exception as e:
|
||||
logger.error(f"1OK adapter call failed url={url}: {e}")
|
||||
return ToolResult(success=False, result=None, error=f"{url} unavailable: {e}")
|
||||
|
||||
async def _crm_search_client(self, args: Dict) -> ToolResult:
|
||||
query = (args or {}).get("query")
|
||||
if not query:
|
||||
return ToolResult(success=False, result=None, error="query is required")
|
||||
return await self._oneok_http_call(self.oneok_crm_url, "/crm/search_client", {"query": query}, method="GET")
|
||||
|
||||
async def _crm_upsert_client(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("client_payload")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="client_payload is required")
|
||||
return await self._oneok_http_call(self.oneok_crm_url, "/crm/upsert_client", payload)
|
||||
|
||||
async def _crm_upsert_site(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("site_payload")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="site_payload is required")
|
||||
return await self._oneok_http_call(self.oneok_crm_url, "/crm/upsert_site", payload)
|
||||
|
||||
async def _crm_upsert_window_unit(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("window_payload")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="window_payload is required")
|
||||
return await self._oneok_http_call(self.oneok_crm_url, "/crm/upsert_window_unit", payload)
|
||||
|
||||
async def _crm_create_quote(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("quote_payload")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="quote_payload is required")
|
||||
return await self._oneok_http_call(self.oneok_crm_url, "/crm/create_quote", payload)
|
||||
|
||||
async def _crm_update_quote(self, args: Dict) -> ToolResult:
|
||||
quote_id = (args or {}).get("quote_id")
|
||||
patch = (args or {}).get("patch")
|
||||
if not quote_id or not isinstance(patch, dict):
|
||||
return ToolResult(success=False, result=None, error="quote_id and patch are required")
|
||||
return await self._oneok_http_call(self.oneok_crm_url, "/crm/update_quote", {"quote_id": quote_id, "patch": patch}, method="PATCH")
|
||||
|
||||
async def _crm_create_job(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("job_payload")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="job_payload is required")
|
||||
return await self._oneok_http_call(self.oneok_crm_url, "/crm/create_job", payload)
|
||||
|
||||
async def _calc_window_quote(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("input_payload")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="input_payload is required")
|
||||
return await self._oneok_http_call(self.oneok_calc_url, "/calc/window_quote", payload)
|
||||
|
||||
async def _docs_render_quote_pdf(self, args: Dict) -> ToolResult:
|
||||
quote_id = (args or {}).get("quote_id")
|
||||
quote_payload = (args or {}).get("quote_payload")
|
||||
if not quote_id and not isinstance(quote_payload, dict):
|
||||
return ToolResult(success=False, result=None, error="quote_id or quote_payload is required")
|
||||
payload = {"quote_id": quote_id, "quote_payload": quote_payload}
|
||||
return await self._oneok_http_call(self.oneok_docs_url, "/docs/render_quote_pdf", payload)
|
||||
|
||||
async def _docs_render_invoice_pdf(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("invoice_payload")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="invoice_payload is required")
|
||||
return await self._oneok_http_call(self.oneok_docs_url, "/docs/render_invoice_pdf", payload)
|
||||
|
||||
async def _schedule_propose_slots(self, args: Dict) -> ToolResult:
|
||||
payload = (args or {}).get("params")
|
||||
if not isinstance(payload, dict):
|
||||
return ToolResult(success=False, result=None, error="params is required")
|
||||
return await self._oneok_http_call(self.oneok_schedule_url, "/schedule/propose_slots", payload)
|
||||
|
||||
async def _schedule_confirm_slot(self, args: Dict) -> ToolResult:
|
||||
job_id = (args or {}).get("job_id")
|
||||
slot = (args or {}).get("slot")
|
||||
if not job_id or slot is None:
|
||||
return ToolResult(success=False, result=None, error="job_id and slot are required")
|
||||
return await self._oneok_http_call(self.oneok_schedule_url, "/schedule/confirm_slot", {"job_id": job_id, "slot": slot})
|
||||
|
||||
async def close(self):
|
||||
await self.http_client.aclose()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user