webagents 0.1.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webagents/__init__.py +18 -0
- webagents/agents/__init__.py +13 -0
- webagents/agents/core/__init__.py +19 -0
- webagents/agents/core/base_agent.py +1834 -0
- webagents/agents/core/handoffs.py +293 -0
- webagents/agents/handoffs/__init__.py +0 -0
- webagents/agents/interfaces/__init__.py +0 -0
- webagents/agents/lifecycle/__init__.py +0 -0
- webagents/agents/skills/__init__.py +109 -0
- webagents/agents/skills/base.py +136 -0
- webagents/agents/skills/core/__init__.py +8 -0
- webagents/agents/skills/core/guardrails/__init__.py +0 -0
- webagents/agents/skills/core/llm/__init__.py +0 -0
- webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
- webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
- webagents/agents/skills/core/llm/litellm/skill.py +538 -0
- webagents/agents/skills/core/llm/openai/__init__.py +1 -0
- webagents/agents/skills/core/llm/xai/__init__.py +1 -0
- webagents/agents/skills/core/mcp/README.md +375 -0
- webagents/agents/skills/core/mcp/__init__.py +15 -0
- webagents/agents/skills/core/mcp/skill.py +731 -0
- webagents/agents/skills/core/memory/__init__.py +11 -0
- webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
- webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
- webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
- webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
- webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
- webagents/agents/skills/core/planning/__init__.py +9 -0
- webagents/agents/skills/core/planning/planner.py +343 -0
- webagents/agents/skills/ecosystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
- webagents/agents/skills/ecosystem/database/__init__.py +1 -0
- webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
- webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
- webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
- webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
- webagents/agents/skills/ecosystem/web/__init__.py +0 -0
- webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
- webagents/agents/skills/robutler/__init__.py +11 -0
- webagents/agents/skills/robutler/auth/README.md +63 -0
- webagents/agents/skills/robutler/auth/__init__.py +17 -0
- webagents/agents/skills/robutler/auth/skill.py +354 -0
- webagents/agents/skills/robutler/crm/__init__.py +18 -0
- webagents/agents/skills/robutler/crm/skill.py +368 -0
- webagents/agents/skills/robutler/discovery/README.md +281 -0
- webagents/agents/skills/robutler/discovery/__init__.py +16 -0
- webagents/agents/skills/robutler/discovery/skill.py +230 -0
- webagents/agents/skills/robutler/kv/__init__.py +6 -0
- webagents/agents/skills/robutler/kv/skill.py +80 -0
- webagents/agents/skills/robutler/message_history/__init__.py +9 -0
- webagents/agents/skills/robutler/message_history/skill.py +270 -0
- webagents/agents/skills/robutler/messages/__init__.py +0 -0
- webagents/agents/skills/robutler/nli/__init__.py +13 -0
- webagents/agents/skills/robutler/nli/skill.py +687 -0
- webagents/agents/skills/robutler/notifications/__init__.py +5 -0
- webagents/agents/skills/robutler/notifications/skill.py +141 -0
- webagents/agents/skills/robutler/payments/__init__.py +41 -0
- webagents/agents/skills/robutler/payments/exceptions.py +255 -0
- webagents/agents/skills/robutler/payments/skill.py +610 -0
- webagents/agents/skills/robutler/storage/__init__.py +10 -0
- webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/files/skill.py +445 -0
- webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/json/skill.py +336 -0
- webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
- webagents/agents/skills/robutler/storage.py +389 -0
- webagents/agents/tools/__init__.py +0 -0
- webagents/agents/tools/decorators.py +426 -0
- webagents/agents/tracing/__init__.py +0 -0
- webagents/agents/workflows/__init__.py +0 -0
- webagents/api/__init__.py +17 -0
- webagents/api/client.py +1207 -0
- webagents/api/types.py +253 -0
- webagents/scripts/__init__.py +0 -0
- webagents/server/__init__.py +28 -0
- webagents/server/context/__init__.py +0 -0
- webagents/server/context/context_vars.py +121 -0
- webagents/server/core/__init__.py +0 -0
- webagents/server/core/app.py +843 -0
- webagents/server/core/middleware.py +69 -0
- webagents/server/core/models.py +98 -0
- webagents/server/core/monitoring.py +59 -0
- webagents/server/endpoints/__init__.py +0 -0
- webagents/server/interfaces/__init__.py +0 -0
- webagents/server/middleware.py +330 -0
- webagents/server/models.py +92 -0
- webagents/server/monitoring.py +659 -0
- webagents/utils/__init__.py +0 -0
- webagents/utils/logging.py +359 -0
- webagents-0.1.12.dist-info/METADATA +99 -0
- webagents-0.1.12.dist-info/RECORD +96 -0
- webagents-0.1.12.dist-info/WHEEL +4 -0
- webagents-0.1.12.dist-info/entry_points.txt +2 -0
- webagents-0.1.12.dist-info/licenses/LICENSE +1 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
"""
|
2
|
+
DiscoverySkill - Simplified Robutler Platform Integration
|
3
|
+
|
4
|
+
Agent discovery skill for Robutler platform.
|
5
|
+
Provides intent-based agent discovery and intent publishing capabilities.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
from typing import Dict, Any, List
|
10
|
+
from dataclasses import dataclass
|
11
|
+
|
12
|
+
from webagents.agents.skills.base import Skill
|
13
|
+
from webagents.agents.tools.decorators import tool, prompt
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class DiscoveryResult:
|
18
|
+
"""Result from intent discovery operation"""
|
19
|
+
agent_id: str
|
20
|
+
intent: str
|
21
|
+
agent_description: str
|
22
|
+
similarity: float
|
23
|
+
url: str
|
24
|
+
rank: int
|
25
|
+
|
26
|
+
|
27
|
+
class DiscoverySkill(Skill):
|
28
|
+
"""
|
29
|
+
Simplified agent discovery skill for Robutler platform
|
30
|
+
|
31
|
+
Key Features:
|
32
|
+
- Intent-based agent discovery via Portal API
|
33
|
+
- Intent publishing for agent registration
|
34
|
+
- Dynamic agent URL configuration for prompts
|
35
|
+
|
36
|
+
Configuration hierarchy for robutler_api_key:
|
37
|
+
1. config.robutler_api_key (explicit configuration)
|
38
|
+
2. agent.api_key (agent's API key)
|
39
|
+
3. ROBUTLER_API_KEY environment variable
|
40
|
+
4. SERVICE_TOKEN environment variable
|
41
|
+
|
42
|
+
Configuration hierarchy for agent_base_url:
|
43
|
+
1. AGENTS_BASE_URL environment variable
|
44
|
+
2. config.agent_base_url (explicit configuration)
|
45
|
+
3. 'http://localhost:2224' (default for local development)
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self, config: Dict[str, Any] = None):
|
49
|
+
super().__init__(config, scope="all")
|
50
|
+
|
51
|
+
# Configuration
|
52
|
+
self.config = config or {}
|
53
|
+
self.enable_discovery = self.config.get('enable_discovery', True)
|
54
|
+
|
55
|
+
# Robutler platform configuration
|
56
|
+
self.robutler_api_url = (
|
57
|
+
os.getenv('ROBUTLER_INTERNAL_API_URL') or
|
58
|
+
os.getenv('ROBUTLER_API_URL') or
|
59
|
+
self.config.get('robutler_api_url') or
|
60
|
+
'https://robutler.ai'
|
61
|
+
)
|
62
|
+
|
63
|
+
# Agent communication base URL configuration (for dynamic agent URLs in prompts)
|
64
|
+
self.agent_base_url = (
|
65
|
+
os.getenv('AGENTS_BASE_URL') or
|
66
|
+
self.config.get('agent_base_url') or
|
67
|
+
'http://localhost:2224' # Default for local development (agents server)
|
68
|
+
)
|
69
|
+
|
70
|
+
# API key: config first (will be resolved in initialize)
|
71
|
+
self.robutler_api_key = self.config.get('robutler_api_key')
|
72
|
+
|
73
|
+
async def initialize(self, agent) -> None:
|
74
|
+
"""Initialize DiscoverySkill"""
|
75
|
+
from webagents.utils.logging import get_logger, log_skill_event
|
76
|
+
|
77
|
+
self.agent = agent
|
78
|
+
self.logger = get_logger('skill.robutler.discovery', self.agent.name)
|
79
|
+
|
80
|
+
# Resolve Robutler API key using hierarchy: config -> agent.api_key -> env
|
81
|
+
if not self.robutler_api_key:
|
82
|
+
if hasattr(self.agent, 'api_key') and self.agent.api_key:
|
83
|
+
self.robutler_api_key = self.agent.api_key
|
84
|
+
self.logger.debug(f"Using agent's API key for Robutler platform")
|
85
|
+
elif os.getenv('ROBUTLER_API_KEY'):
|
86
|
+
self.robutler_api_key = os.getenv('ROBUTLER_API_KEY')
|
87
|
+
self.logger.debug(f"Using ROBUTLER_API_KEY from environment")
|
88
|
+
elif os.getenv('SERVICE_TOKEN'):
|
89
|
+
self.robutler_api_key = os.getenv('SERVICE_TOKEN')
|
90
|
+
self.logger.debug(f"Using SERVICE_TOKEN from environment")
|
91
|
+
else:
|
92
|
+
self.logger.warning("No API key configured for discovery")
|
93
|
+
|
94
|
+
log_skill_event(self.agent.name, 'discovery', 'initialized', {
|
95
|
+
'enable_discovery': self.enable_discovery,
|
96
|
+
'robutler_api_url': self.robutler_api_url,
|
97
|
+
'agent_base_url': self.agent_base_url,
|
98
|
+
'has_api_key': bool(self.robutler_api_key)
|
99
|
+
})
|
100
|
+
|
101
|
+
# ===== DISCOVERY TOOLS =====
|
102
|
+
|
103
|
+
@prompt(priority=20, scope="all")
|
104
|
+
def discovery_general_prompt(self, context: Any = None) -> str:
|
105
|
+
base_url = self.agent_base_url.rstrip('/')
|
106
|
+
return f"You are part of a network of AI agents working for their owners. Each agent has their own name and address. @agentname corresponds to {base_url}/agents/agentname. Use discovery_tool to discover other agents and their capabilities."
|
107
|
+
|
108
|
+
@tool(description="Discover agents and their capabilities across the Robutler network by intent. Make the intent as specific as possible. When presenting the results, make it simple and non-technical.", scope="all")
|
109
|
+
async def discovery_tool(self,
|
110
|
+
intent: str,
|
111
|
+
top_k: int = 10,
|
112
|
+
context=None) -> Dict[str, Any]:
|
113
|
+
"""Discover agents by intent across the Robutler network"""
|
114
|
+
if not self.enable_discovery:
|
115
|
+
return {'success': False, 'error': 'Discovery disabled'}
|
116
|
+
|
117
|
+
if not self.robutler_api_key:
|
118
|
+
return {'success': False, 'error': 'API key not configured for discovery'}
|
119
|
+
|
120
|
+
try:
|
121
|
+
import aiohttp
|
122
|
+
|
123
|
+
# Clamp top_k between 1-50
|
124
|
+
limited_top_k = max(1, min(50, top_k))
|
125
|
+
|
126
|
+
async with aiohttp.ClientSession() as session:
|
127
|
+
async with session.post(
|
128
|
+
f"{self.robutler_api_url}/api/intents/search",
|
129
|
+
headers={
|
130
|
+
'Authorization': f'Bearer {self.robutler_api_key}',
|
131
|
+
'Content-Type': 'application/json',
|
132
|
+
},
|
133
|
+
json={
|
134
|
+
'intent': intent.strip(),
|
135
|
+
'top_k': limited_top_k,
|
136
|
+
}
|
137
|
+
) as response:
|
138
|
+
|
139
|
+
if not response.ok:
|
140
|
+
raise Exception(f"Discovery API error: {response.status}")
|
141
|
+
|
142
|
+
result = await response.json()
|
143
|
+
results = result.get('data', {}).get('results', [])
|
144
|
+
|
145
|
+
return {
|
146
|
+
'success': True,
|
147
|
+
'intent': intent,
|
148
|
+
'results_count': len(results),
|
149
|
+
'results': [
|
150
|
+
{
|
151
|
+
'agent_id': r.get('agent_id'),
|
152
|
+
'intent': r.get('intent'),
|
153
|
+
'description': r.get('agent_description'),
|
154
|
+
'similarity': r.get('similarity'),
|
155
|
+
'url': r.get('url'),
|
156
|
+
'rank': r.get('rank'),
|
157
|
+
}
|
158
|
+
for r in results
|
159
|
+
]
|
160
|
+
}
|
161
|
+
|
162
|
+
except Exception as e:
|
163
|
+
self.logger.error(f"Agent discovery failed: {e}")
|
164
|
+
return {
|
165
|
+
'success': False,
|
166
|
+
'intent': intent,
|
167
|
+
'results_count': 0,
|
168
|
+
'results': [],
|
169
|
+
'error': str(e)
|
170
|
+
}
|
171
|
+
|
172
|
+
@tool(description="Publish agent intents to the platform", scope="owner")
|
173
|
+
async def publish_intents_tool(self,
|
174
|
+
intents: List[str],
|
175
|
+
description: str,
|
176
|
+
context=None) -> Dict[str, Any]:
|
177
|
+
"""Publish agent intents to the Robutler platform"""
|
178
|
+
if not self.enable_discovery:
|
179
|
+
return {'success': False, 'error': 'Discovery disabled'}
|
180
|
+
|
181
|
+
if not self.robutler_api_key:
|
182
|
+
return {'success': False, 'error': 'API key not configured for discovery'}
|
183
|
+
|
184
|
+
try:
|
185
|
+
import aiohttp
|
186
|
+
|
187
|
+
# Get agent information
|
188
|
+
agent_id = getattr(self.agent, 'name', 'unknown')
|
189
|
+
agent_url = self.config.get('agent_url', f"https://robutler.ai/agents/{agent_id}")
|
190
|
+
|
191
|
+
# Prepare intent data
|
192
|
+
intents_data = [
|
193
|
+
{
|
194
|
+
'intent': intent,
|
195
|
+
'agent_id': agent_id,
|
196
|
+
'description': description,
|
197
|
+
'url': agent_url,
|
198
|
+
}
|
199
|
+
for intent in intents
|
200
|
+
]
|
201
|
+
|
202
|
+
async with aiohttp.ClientSession() as session:
|
203
|
+
async with session.post(
|
204
|
+
f"{self.robutler_api_url}/api/intents/publish",
|
205
|
+
headers={
|
206
|
+
'Authorization': f'Bearer {self.robutler_api_key}',
|
207
|
+
'Content-Type': 'application/json',
|
208
|
+
},
|
209
|
+
json={'intents': intents_data}
|
210
|
+
) as response:
|
211
|
+
|
212
|
+
if not response.ok:
|
213
|
+
raise Exception(f"Publish API error: {response.status}")
|
214
|
+
|
215
|
+
result = await response.json()
|
216
|
+
|
217
|
+
return {
|
218
|
+
'success': True,
|
219
|
+
'agent_id': agent_id,
|
220
|
+
'published_intents': intents,
|
221
|
+
'results': result
|
222
|
+
}
|
223
|
+
|
224
|
+
except Exception as e:
|
225
|
+
self.logger.error(f"Intent publishing failed: {e}")
|
226
|
+
return {'success': False, 'error': str(e)}
|
227
|
+
|
228
|
+
def get_dependencies(self) -> List[str]:
|
229
|
+
"""Get skill dependencies"""
|
230
|
+
return ['aiohttp'] # Required for HTTP client
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Any, Dict, Optional
|
3
|
+
|
4
|
+
import httpx
|
5
|
+
|
6
|
+
from webagents.agents.skills.base import Skill
|
7
|
+
from webagents.agents.tools.decorators import tool
|
8
|
+
from webagents.server.context.context_vars import get_context
|
9
|
+
|
10
|
+
|
11
|
+
class KVSkill(Skill):
|
12
|
+
"""Simple per-agent key-value storage via portal /api/kv.
|
13
|
+
|
14
|
+
Scope: owner-only tools to ensure only the agent owner can read/write.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None) -> None:
|
18
|
+
super().__init__(config or {}, scope="all")
|
19
|
+
self.portal_url = os.getenv('ROBUTLER_INTERNAL_API_URL') or os.getenv('ROBUTLER_API_URL', 'http://localhost:3000')
|
20
|
+
|
21
|
+
async def _resolve_agent_and_user(self) -> Optional[Dict[str, str]]:
|
22
|
+
try:
|
23
|
+
ctx = get_context()
|
24
|
+
agent = getattr(ctx, 'agent', None)
|
25
|
+
auth = getattr(ctx, 'auth', None) or (ctx and ctx.get('auth'))
|
26
|
+
agent_id = getattr(agent, 'id', None)
|
27
|
+
user_id = getattr(auth, 'user_id', None)
|
28
|
+
if agent_id and user_id:
|
29
|
+
return {"agent_id": agent_id, "user_id": user_id}
|
30
|
+
except Exception:
|
31
|
+
pass
|
32
|
+
return None
|
33
|
+
|
34
|
+
async def _headers(self) -> Dict[str, str]:
|
35
|
+
# Prefer agent API key for auth
|
36
|
+
api_key = getattr(self.agent, 'api_key', None)
|
37
|
+
return {"X-API-Key": api_key} if api_key else {}
|
38
|
+
|
39
|
+
@tool(description="Set a key to a string value (owner-only)", scope="owner")
|
40
|
+
async def kv_set(self, key: str, value: str, namespace: Optional[str] = None) -> str:
|
41
|
+
ids = await self._resolve_agent_and_user()
|
42
|
+
if not ids:
|
43
|
+
return "❌ Missing agent/user context"
|
44
|
+
body = {"agentId": ids["agent_id"], "key": key, "value": value, "namespace": namespace}
|
45
|
+
async with httpx.AsyncClient(timeout=15) as client:
|
46
|
+
resp = await client.post(f"{self.portal_url}/api/kv", json=body, headers=await self._headers())
|
47
|
+
if resp.status_code >= 400:
|
48
|
+
return f"❌ KV set failed: {resp.text}"
|
49
|
+
return "✅ Saved"
|
50
|
+
|
51
|
+
@tool(description="Get a string value by key (owner-only)", scope="owner")
|
52
|
+
async def kv_get(self, key: str, namespace: Optional[str] = None) -> str:
|
53
|
+
ids = await self._resolve_agent_and_user()
|
54
|
+
if not ids:
|
55
|
+
return ""
|
56
|
+
params = {"agentId": ids["agent_id"], "key": key}
|
57
|
+
if namespace:
|
58
|
+
params["namespace"] = namespace
|
59
|
+
async with httpx.AsyncClient(timeout=15) as client:
|
60
|
+
resp = await client.get(f"{self.portal_url}/api/kv", params=params, headers=await self._headers())
|
61
|
+
if resp.status_code >= 400:
|
62
|
+
return ""
|
63
|
+
data = resp.json()
|
64
|
+
return data.get('value') or ""
|
65
|
+
|
66
|
+
@tool(description="Delete a key (owner-only)", scope="owner")
|
67
|
+
async def kv_delete(self, key: str, namespace: Optional[str] = None) -> str:
|
68
|
+
ids = await self._resolve_agent_and_user()
|
69
|
+
if not ids:
|
70
|
+
return ""
|
71
|
+
params = {"agentId": ids["agent_id"], "key": key}
|
72
|
+
if namespace:
|
73
|
+
params["namespace"] = namespace
|
74
|
+
async with httpx.AsyncClient(timeout=15) as client:
|
75
|
+
resp = await client.delete(f"{self.portal_url}/api/kv", params=params, headers=await self._headers())
|
76
|
+
if resp.status_code >= 400:
|
77
|
+
return ""
|
78
|
+
return "🗑️ Deleted"
|
79
|
+
|
80
|
+
|
@@ -0,0 +1,270 @@
|
|
1
|
+
"""
|
2
|
+
MessageHistorySkill - Save agent conversation messages to Robutler Portal
|
3
|
+
|
4
|
+
This skill enables agents to:
|
5
|
+
- Automatically save conversation messages on connection finalization
|
6
|
+
- Create and manage conversation threads
|
7
|
+
- Retrieve conversation history for context
|
8
|
+
- Support agent-to-agent and user-to-agent conversations
|
9
|
+
"""
|
10
|
+
|
11
|
+
import os
|
12
|
+
import json
|
13
|
+
import asyncio
|
14
|
+
from typing import Dict, Any, List, Optional
|
15
|
+
from webagents.agents.skills.base import Skill
|
16
|
+
from webagents.agents.tools.decorators import tool, hook
|
17
|
+
from webagents.utils.logging import get_logger, log_skill_event
|
18
|
+
|
19
|
+
|
20
|
+
class MessageHistorySkill(Skill):
|
21
|
+
"""
|
22
|
+
Skill for automatically saving agent conversation history in Robutler Portal
|
23
|
+
|
24
|
+
Features:
|
25
|
+
- Automatic message saving on connection finalization
|
26
|
+
- Create and manage conversation threads
|
27
|
+
- Support for agent-to-agent and user-to-agent conversations
|
28
|
+
- Retrieve conversation history for context
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, config: Dict[str, Any] = None):
|
32
|
+
super().__init__(config, scope="all")
|
33
|
+
|
34
|
+
# Configuration
|
35
|
+
self.portal_url = config.get('portal_url') if config else None
|
36
|
+
self.api_key = config.get('api_key') if config else None
|
37
|
+
|
38
|
+
# Runtime state
|
39
|
+
self.current_conversation_id = None
|
40
|
+
self.agent_id = None
|
41
|
+
self.api_client = None
|
42
|
+
self.session_messages = []
|
43
|
+
|
44
|
+
async def initialize(self, agent: 'BaseAgent') -> None:
|
45
|
+
"""Initialize the message history skill"""
|
46
|
+
from webagents.utils.logging import get_logger, log_skill_event
|
47
|
+
|
48
|
+
self.agent = agent
|
49
|
+
self.agent_id = getattr(agent, 'id', None) or getattr(agent, 'name', 'unknown')
|
50
|
+
self.logger = get_logger('skill.message_history', agent.name)
|
51
|
+
|
52
|
+
# Initialize API client
|
53
|
+
try:
|
54
|
+
from webagents.api.client import RobutlerClient
|
55
|
+
|
56
|
+
# Get API key from agent config, skill config, or environment
|
57
|
+
api_key = (
|
58
|
+
self.api_key or
|
59
|
+
getattr(agent, 'api_key', None) or
|
60
|
+
os.getenv('ROBUTLER_API_KEY')
|
61
|
+
)
|
62
|
+
|
63
|
+
if not api_key:
|
64
|
+
self.logger.warning("No API key available for MessageHistorySkill")
|
65
|
+
return
|
66
|
+
|
67
|
+
self.api_client = RobutlerClient(
|
68
|
+
api_key=api_key,
|
69
|
+
base_url=self.portal_url or os.getenv('ROBUTLER_API_URL', 'https://robutler.ai')
|
70
|
+
)
|
71
|
+
|
72
|
+
log_skill_event(agent.name, 'message_history', 'initialized', {
|
73
|
+
'agent_id': self.agent_id
|
74
|
+
})
|
75
|
+
|
76
|
+
except ImportError:
|
77
|
+
self.logger.warning("RobutlerClient not available - MessageHistorySkill disabled")
|
78
|
+
except Exception as e:
|
79
|
+
self.logger.error(f"Failed to initialize MessageHistorySkill: {e}")
|
80
|
+
|
81
|
+
@tool(description="Create a new conversation thread for organizing messages")
|
82
|
+
async def create_conversation(
|
83
|
+
self,
|
84
|
+
title: str,
|
85
|
+
other_party_id: str,
|
86
|
+
other_party_type: str = "user",
|
87
|
+
visibility: str = "private"
|
88
|
+
) -> str:
|
89
|
+
"""Create a new conversation thread"""
|
90
|
+
if not self.api_client:
|
91
|
+
return "❌ Error: API client not initialized"
|
92
|
+
|
93
|
+
try:
|
94
|
+
import aiohttp
|
95
|
+
|
96
|
+
async with aiohttp.ClientSession() as session:
|
97
|
+
headers = {
|
98
|
+
'Authorization': f'Bearer {self.api_client.api_key}',
|
99
|
+
'Content-Type': 'application/json'
|
100
|
+
}
|
101
|
+
|
102
|
+
payload = {
|
103
|
+
'operation': 'createChat',
|
104
|
+
'agentId': self.agent_id,
|
105
|
+
'title': title,
|
106
|
+
'visibility': visibility,
|
107
|
+
'otherPartyId': other_party_id,
|
108
|
+
'otherPartyType': other_party_type
|
109
|
+
}
|
110
|
+
|
111
|
+
async with session.post(
|
112
|
+
f"{self.api_client.base_url}/api/chat",
|
113
|
+
headers=headers,
|
114
|
+
json=payload
|
115
|
+
) as response:
|
116
|
+
if response.status == 200:
|
117
|
+
data = await response.json()
|
118
|
+
conversation_id = data.get('id')
|
119
|
+
self.current_conversation_id = conversation_id
|
120
|
+
return f"✅ Created conversation: {conversation_id}"
|
121
|
+
else:
|
122
|
+
error_text = await response.text()
|
123
|
+
return f"❌ Failed to create conversation: {response.status} - {error_text}"
|
124
|
+
|
125
|
+
except Exception as e:
|
126
|
+
self.logger.error(f"Error creating conversation: {e}")
|
127
|
+
return f"❌ Error creating conversation: {str(e)}"
|
128
|
+
|
129
|
+
@tool(description="Retrieve conversation history for context")
|
130
|
+
async def get_conversation_history(
|
131
|
+
self,
|
132
|
+
conversation_id: str = None,
|
133
|
+
limit: int = 50
|
134
|
+
) -> str:
|
135
|
+
"""Retrieve conversation history"""
|
136
|
+
if not self.api_client:
|
137
|
+
return "❌ Error: API client not initialized"
|
138
|
+
|
139
|
+
try:
|
140
|
+
chat_id = conversation_id or self.current_conversation_id
|
141
|
+
if not chat_id:
|
142
|
+
return "❌ Error: No conversation ID provided and no current conversation"
|
143
|
+
|
144
|
+
import aiohttp
|
145
|
+
|
146
|
+
async with aiohttp.ClientSession() as session:
|
147
|
+
headers = {
|
148
|
+
'Authorization': f'Bearer {self.api_client.api_key}',
|
149
|
+
'Content-Type': 'application/json'
|
150
|
+
}
|
151
|
+
|
152
|
+
params = {
|
153
|
+
'operation': 'getMessagesByChatId',
|
154
|
+
'chatId': chat_id
|
155
|
+
}
|
156
|
+
|
157
|
+
async with session.get(
|
158
|
+
f"{self.api_client.base_url}/api/messages",
|
159
|
+
headers=headers,
|
160
|
+
params=params
|
161
|
+
) as response:
|
162
|
+
if response.status == 200:
|
163
|
+
data = await response.json()
|
164
|
+
messages = data.get('messages', [])
|
165
|
+
|
166
|
+
# Format messages for context
|
167
|
+
formatted_messages = []
|
168
|
+
for msg in messages[-limit:]: # Get last N messages
|
169
|
+
sender_info = f"{msg.get('senderType', 'unknown')}:{msg.get('senderId', 'unknown')}"
|
170
|
+
content = msg.get('parts', '')
|
171
|
+
if isinstance(content, list):
|
172
|
+
content = ' '.join(str(part) for part in content)
|
173
|
+
formatted_messages.append(f"[{sender_info}] {content}")
|
174
|
+
|
175
|
+
return "\n".join(formatted_messages)
|
176
|
+
else:
|
177
|
+
error_text = await response.text()
|
178
|
+
return f"❌ Failed to get conversation history: {response.status} - {error_text}"
|
179
|
+
|
180
|
+
except Exception as e:
|
181
|
+
self.logger.error(f"Error getting conversation history: {e}")
|
182
|
+
return f"❌ Error getting conversation history: {str(e)}"
|
183
|
+
|
184
|
+
@tool(description="Set the current active conversation")
|
185
|
+
async def set_current_conversation(self, conversation_id: str) -> str:
|
186
|
+
"""Set the current active conversation"""
|
187
|
+
self.current_conversation_id = conversation_id
|
188
|
+
return f"✅ Set current conversation to: {conversation_id}"
|
189
|
+
|
190
|
+
@hook("on_message")
|
191
|
+
async def capture_session_messages(self, context):
|
192
|
+
"""Capture messages during the session for later saving"""
|
193
|
+
try:
|
194
|
+
# Extract message information from context
|
195
|
+
messages = getattr(context, 'messages', [])
|
196
|
+
if messages:
|
197
|
+
# Store the full message exchange for this session
|
198
|
+
self.session_messages = messages.copy()
|
199
|
+
self.logger.debug(f"Captured {len(messages)} messages for session")
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
self.logger.warning(f"Failed to capture session messages: {e}")
|
203
|
+
|
204
|
+
return context
|
205
|
+
|
206
|
+
@hook("finalize_connection")
|
207
|
+
async def save_session_messages(self, context):
|
208
|
+
"""Automatically save all session messages when connection finalizes"""
|
209
|
+
if not self.api_client or not self.session_messages or not self.current_conversation_id:
|
210
|
+
return context
|
211
|
+
|
212
|
+
try:
|
213
|
+
# Prepare messages for saving
|
214
|
+
messages_to_save = []
|
215
|
+
|
216
|
+
for i, msg in enumerate(self.session_messages):
|
217
|
+
# Determine sender info based on message role
|
218
|
+
if msg.get('role') == 'user':
|
219
|
+
sender_id = "user_session" # Default user ID - could be enhanced
|
220
|
+
sender_type = "user"
|
221
|
+
elif msg.get('role') == 'assistant':
|
222
|
+
sender_id = self.agent_id
|
223
|
+
sender_type = "agent"
|
224
|
+
else:
|
225
|
+
continue # Skip system messages
|
226
|
+
|
227
|
+
message_data = {
|
228
|
+
'id': f"msg_{self.current_conversation_id}_{i}_{hash(str(msg))}",
|
229
|
+
'chatId': self.current_conversation_id,
|
230
|
+
'role': msg.get('role'),
|
231
|
+
'parts': msg.get('content', ''),
|
232
|
+
'attachments': json.dumps([]),
|
233
|
+
'senderId': sender_id,
|
234
|
+
'senderType': sender_type
|
235
|
+
}
|
236
|
+
messages_to_save.append(message_data)
|
237
|
+
|
238
|
+
if messages_to_save:
|
239
|
+
# Save messages via API
|
240
|
+
import aiohttp
|
241
|
+
|
242
|
+
async with aiohttp.ClientSession() as session:
|
243
|
+
headers = {
|
244
|
+
'Authorization': f'Bearer {self.api_client.api_key}',
|
245
|
+
'Content-Type': 'application/json'
|
246
|
+
}
|
247
|
+
|
248
|
+
payload = {
|
249
|
+
'operation': 'saveMessages',
|
250
|
+
'messages': messages_to_save
|
251
|
+
}
|
252
|
+
|
253
|
+
async with session.post(
|
254
|
+
f"{self.api_client.base_url}/api/messages",
|
255
|
+
headers=headers,
|
256
|
+
json=payload
|
257
|
+
) as response:
|
258
|
+
if response.status == 200:
|
259
|
+
self.logger.info(f"Successfully saved {len(messages_to_save)} messages to conversation {self.current_conversation_id}")
|
260
|
+
else:
|
261
|
+
error_text = await response.text()
|
262
|
+
self.logger.error(f"Failed to save messages: {response.status} - {error_text}")
|
263
|
+
|
264
|
+
# Clear session messages after saving
|
265
|
+
self.session_messages = []
|
266
|
+
|
267
|
+
except Exception as e:
|
268
|
+
self.logger.error(f"Error saving session messages: {e}")
|
269
|
+
|
270
|
+
return context
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
NLI Skill - Natural Language Interface for Agent-to-Agent Communication
|
3
|
+
|
4
|
+
Provides HTTP-based communication between Robutler agents with natural language messages.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .skill import NLISkill, NLICommunication, AgentEndpoint
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
'NLISkill',
|
11
|
+
'NLICommunication',
|
12
|
+
'AgentEndpoint'
|
13
|
+
]
|