runtime: sync router/gateway/config policy and clan role registry

This commit is contained in:
Apple
2026-02-19 00:14:06 -08:00
parent 675b25953b
commit dfc0ef1ceb
35 changed files with 6141 additions and 498 deletions

View File

@@ -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'],

View File

@@ -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
)
]
)

View File

@@ -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

View File

@@ -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()