dnotifier 1.0.2__tar.gz → 1.0.4__tar.gz
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.
- dnotifier-1.0.4/CHANGELOG.md +6 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/LICENSE +4 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/PKG-INFO +1 -1
- {dnotifier-1.0.2 → dnotifier-1.0.4}/pyproject.toml +1 -1
- dnotifier-1.0.4/src/dnotifier/__init__.py +7 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/__init__.py +17 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/backend_client.py +81 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/descriptors.py +6 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/errors.py +25 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/ids.py +13 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/local_completions.py +140 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/mcp/__init__.py +0 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/mcp/client.py +72 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/mcp/registry.py +35 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/observability.py +14 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/permissions.py +12 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/runtime.py +162 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/schema.py +13 -0
- dnotifier-1.0.4/src/dnotifier/agent_runtime/tool_registry.py +26 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/config/endpoints.py +9 -2
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/dnotifier.py +158 -122
- dnotifier-1.0.4/src/dnotifier/transport/http_transport.py +13 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/utils/logs.py +5 -5
- dnotifier-1.0.4/src/dnotifier/workflow/agent.py +23 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/context.py +69 -62
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/step_types.py +3 -2
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/workflow_observability_client.py +10 -9
- dnotifier-1.0.2/CHANGELOG.md +0 -7
- dnotifier-1.0.2/src/dnotifier/__init__.py +0 -6
- dnotifier-1.0.2/src/dnotifier/transport/http_transport.py +0 -13
- dnotifier-1.0.2/src/dnotifier/workflow/agent.py +0 -12
- {dnotifier-1.0.2 → dnotifier-1.0.4}/.gitignore +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/README.md +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/config/__init__.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/transport/__init__.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/transport/websocket_transport.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/utils/__init__.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/utils/buffer.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/utils/frame.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/utils/id.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/utils/payload.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/__init__.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/errors.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/graph_parents.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/ids.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/json_payload.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/observability.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/remote_agent_client.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/remote_agent_protocol.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/runner.py +0 -0
- {dnotifier-1.0.2 → dnotifier-1.0.4}/src/dnotifier/workflow/workflow.py +0 -0
|
@@ -4,3 +4,7 @@ This software and associated documentation files (the "Software") are the
|
|
|
4
4
|
property of DNotifier. Unauthorized copying, modification, distribution, or
|
|
5
5
|
use of the Software, in whole or in part, is strictly prohibited without
|
|
6
6
|
express written permission from DNotifier.
|
|
7
|
+
|
|
8
|
+
For licensing inquiries, contact DNotifier.
|
|
9
|
+
Website: https://dnotifier.com
|
|
10
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dnotifier
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Real-time notification and messaging SDK for Python. WebSocket & HTTP, auth, binary frames, AI, RAG, and workflows.
|
|
5
5
|
Project-URL: Homepage, https://dnotifier.com
|
|
6
6
|
Project-URL: Documentation, https://dnotifier.gitbook.io/product-docs
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dnotifier"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Real-time notification and messaging SDK for Python. WebSocket & HTTP, auth, binary frames, AI, RAG, and workflows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Proprietary" }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from.agent_runtime.errors import AgentExecutionError,BackendError,DNotifierAgentError,LLMError,MCPConnectionError,MCPToolError,PermissionDeniedError,ToolExecutionError,ToolNotFoundError,ToolValidationError
|
|
3
|
+
from.dnotifier import DNotifier,DNotifierMessage,DNotifierMessageMetadata,DNotifierPlanLimits
|
|
4
|
+
from.utils.payload import Payload
|
|
5
|
+
from.workflow import Agent,Workflow,WorkflowContext,WorkflowError,WorkflowRunner,define_agent
|
|
6
|
+
__version__='1.0.3'
|
|
7
|
+
__all__=['DNotifier','DNotifierMessage','DNotifierMessageMetadata','DNotifierPlanLimits','Payload','Agent','define_agent','Workflow','WorkflowContext','WorkflowRunner','WorkflowError','DNotifierAgentError','ToolNotFoundError','ToolValidationError','ToolExecutionError','PermissionDeniedError','MCPConnectionError','MCPToolError','AgentExecutionError','BackendError','LLMError','__version__']
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A='backend'
|
|
3
|
+
from typing import Any,Callable,Literal,Optional
|
|
4
|
+
from.backend_client import AgentBackendClient,AgentSync
|
|
5
|
+
from.errors import AgentExecutionError,BackendError,DNotifierAgentError,LLMError,MCPConnectionError,MCPToolError,PermissionDeniedError,ToolExecutionError,ToolNotFoundError,ToolValidationError
|
|
6
|
+
from.mcp.registry import McpRegistry
|
|
7
|
+
from.observability import AgentObservability
|
|
8
|
+
from.runtime import run_agent_loop,run_tool_agent,workflow_trace_from_context
|
|
9
|
+
from.tool_registry import ToolRegistry
|
|
10
|
+
class AgentServices:
|
|
11
|
+
def __init__(A,*,backend,sync,events,tools,mcp,permission_enforcement):A.backend=backend;A.sync=sync;A.events=events;A.tools=tools;A.mcp=mcp;A.permission_enforcement=permission_enforcement
|
|
12
|
+
def configure(A,*,token=None,on_agent_event=...):
|
|
13
|
+
C=on_agent_event;B=token
|
|
14
|
+
if B is not None:A.backend.configure(token=B)
|
|
15
|
+
if C is not...:A.events.on_agent_event=C
|
|
16
|
+
def create_agent_services(*,permission_enforcement=_A,on_agent_event=None):E='disabled';A=AgentBackendClient();B=AgentSync(backend=A);C=AgentObservability(backend=A,on_agent_event=on_agent_event);D=ToolRegistry(sync=B);F=McpRegistry(tools=D,sync=B,events=C);return AgentServices(backend=A,sync=B,events=C,tools=D,mcp=F,permission_enforcement=E if permission_enforcement==E else _A)
|
|
17
|
+
__all__=['create_agent_services','run_agent_loop','run_tool_agent','workflow_trace_from_context','AgentServices','DNotifierAgentError','ToolNotFoundError','ToolValidationError','ToolExecutionError','PermissionDeniedError','MCPConnectionError','MCPToolError','AgentExecutionError','BackendError','LLMError']
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_F='server'
|
|
3
|
+
_E='tools'
|
|
4
|
+
_D='name'
|
|
5
|
+
_C='sdkVersion'
|
|
6
|
+
_B='id'
|
|
7
|
+
_A=None
|
|
8
|
+
import asyncio,logging
|
|
9
|
+
from typing import Any,Optional,TYPE_CHECKING
|
|
10
|
+
from..config.endpoints import DNotifierEndpoints
|
|
11
|
+
from..transport.http_transport import HttpTransport
|
|
12
|
+
from.descriptors import to_public_tool
|
|
13
|
+
from.errors import BackendError
|
|
14
|
+
from.ids import agent_backend_id
|
|
15
|
+
if TYPE_CHECKING:from..workflow.agent import Agent
|
|
16
|
+
SDK_VERSION='1.0.3'
|
|
17
|
+
logger=logging.getLogger('dnotifier')
|
|
18
|
+
def _unwrap(body):
|
|
19
|
+
E='_responseData';D='_responseCode';A=body
|
|
20
|
+
if isinstance(A,dict)and A.get(D)is not _A:
|
|
21
|
+
C=int(A[D])
|
|
22
|
+
if C!=200:
|
|
23
|
+
B=A.get(E)
|
|
24
|
+
if not isinstance(B,str):B=str(B or'')
|
|
25
|
+
raise RuntimeError(B or f"Backend response {C}")
|
|
26
|
+
return A.get(E,A)
|
|
27
|
+
return A
|
|
28
|
+
class AgentBackendClient:
|
|
29
|
+
def __init__(A,*,token=_A,request_timeout_ms=30000):A.token=token;A.request_timeout_ms=request_timeout_ms
|
|
30
|
+
def configure(B,*,token=_A):
|
|
31
|
+
A=token
|
|
32
|
+
if A is not _A:B.token=A
|
|
33
|
+
async def post(B,url,body):
|
|
34
|
+
if not B.token:raise BackendError('Agent backend call requires a connected client')
|
|
35
|
+
C=HttpTransport(url,headers={'authorization':f"{B.token}"},timeout_ms=B.request_timeout_ms)
|
|
36
|
+
try:return _unwrap(await C.send(body))
|
|
37
|
+
except BackendError:raise
|
|
38
|
+
except Exception as A:raise BackendError(str(A)or'Backend request failed',cause=A if isinstance(A,BaseException)else _A)from A
|
|
39
|
+
async def post_quiet(A,url,body):
|
|
40
|
+
if not A.token:return
|
|
41
|
+
try:await A.post(url,body)
|
|
42
|
+
except Exception:pass
|
|
43
|
+
async def register_tool(A,tool):await A.post(DNotifierEndpoints.agent_tool_register_url,{**to_public_tool(tool),_C:SDK_VERSION})
|
|
44
|
+
async def register_mcp_server(C,server):B='transport';A=server;await C.post(DNotifierEndpoints.agent_mcp_server_register_url,{_B:A[_B],_D:A[_D],B:A[B],_C:SDK_VERSION})
|
|
45
|
+
async def register_mcp_server_tools(A,server_id,tools):await A.post(DNotifierEndpoints.agent_mcp_server_tools_url(server_id),{_E:[to_public_tool(A)for A in tools],_C:SDK_VERSION})
|
|
46
|
+
async def register_agent(B,agent):G='permissions';F='toolIds';E='mcpServerId';A=agent;C=agent_backend_id(A.name);D=A.tools or[];H=list({A.get(E)for A in D if isinstance(A,dict)and A.get(E)});await B.post(DNotifierEndpoints.agent_register_url,{_B:C,_D:A.name,'description':A.description or'',F:[A[_B]for A in D],'mcpServerIds':H,G:A.permissions or{},_C:SDK_VERSION});await B.post(DNotifierEndpoints.agent_tools_url(C),{F:[A[_B]for A in D]});await B.post(DNotifierEndpoints.agent_permissions_url(C),{G:A.permissions or{}})
|
|
47
|
+
async def check_permission(B,body):A=await B.post(DNotifierEndpoints.agent_permission_check_url,body);return A if isinstance(A,dict)else{}
|
|
48
|
+
class AgentSync:
|
|
49
|
+
def __init__(A,*,backend):A.backend=backend;A._tools={};A._servers={};A._agents={};A._lock=asyncio.Lock()
|
|
50
|
+
def enqueue_tool(A,tool):
|
|
51
|
+
A._tools[tool[_B]]=tool
|
|
52
|
+
if A.backend.token:
|
|
53
|
+
try:B=asyncio.get_running_loop();B.create_task(A._safe_flush())
|
|
54
|
+
except RuntimeError:pass
|
|
55
|
+
def enqueue_server(A,server,tools):
|
|
56
|
+
B=server;A._servers[B[_B]]={_F:B,_E:tools}
|
|
57
|
+
if A.backend.token:
|
|
58
|
+
try:C=asyncio.get_running_loop();C.create_task(A._safe_flush())
|
|
59
|
+
except RuntimeError:pass
|
|
60
|
+
def enqueue_agent(B,agent):A=agent;B._agents[agent_backend_id(A.name)]=A
|
|
61
|
+
async def _safe_flush(A):
|
|
62
|
+
try:await A.flush()
|
|
63
|
+
except Exception:pass
|
|
64
|
+
async def flush(A):
|
|
65
|
+
async with A._lock:await A._flush_once()
|
|
66
|
+
async def _flush_once(A):
|
|
67
|
+
if not A.backend.token:return
|
|
68
|
+
B=0
|
|
69
|
+
for(E,F)in list(A._tools.items()):
|
|
70
|
+
try:await A.backend.register_tool(F)
|
|
71
|
+
except Exception:B+=1
|
|
72
|
+
A._tools.pop(E,_A)
|
|
73
|
+
for(C,D)in list(A._servers.items()):
|
|
74
|
+
try:await A.backend.register_mcp_server(D[_F]);await A.backend.register_mcp_server_tools(C,D[_E])
|
|
75
|
+
except Exception:B+=1
|
|
76
|
+
A._servers.pop(C,_A)
|
|
77
|
+
for(G,H)in list(A._agents.items()):
|
|
78
|
+
try:await A.backend.register_agent(H)
|
|
79
|
+
except Exception:B+=1
|
|
80
|
+
A._agents.pop(G,_A)
|
|
81
|
+
if B>0:logger.warning('[dnotifier] skipped %s agent-backend register call(s) (routes not available yet)',B)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
def to_public_tool(tool):
|
|
4
|
+
G='provider';F='inputSchema';E='description';D='name';B='mcpServerId';A=tool;C={'id':A['id'],D:A[D],E:A.get(E)or'',F:A.get(F)or{'type':'object'},G:A[G]}
|
|
5
|
+
if A.get(B):C[B]=A[B]
|
|
6
|
+
return C
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
from typing import Any,Optional
|
|
4
|
+
class DNotifierAgentError(Exception):
|
|
5
|
+
def __init__(A,message,*,execution_id=_A,agent_id=_A,tool_id=_A,provider=_A,cause=_A,**C):
|
|
6
|
+
B=cause;super().__init__(message);A.name='DNotifierAgentError';A.execution_id=execution_id;A.agent_id=agent_id;A.tool_id=tool_id;A.provider=provider
|
|
7
|
+
if B is not _A:A.__cause__=B
|
|
8
|
+
class ToolNotFoundError(DNotifierAgentError):
|
|
9
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='ToolNotFoundError'
|
|
10
|
+
class ToolValidationError(DNotifierAgentError):
|
|
11
|
+
def __init__(A,message,fields=_A,issues=_A,**B):C={**(fields or{}),**B};super().__init__(message,**C);A.name='ToolValidationError';A.issues=issues or[]
|
|
12
|
+
class ToolExecutionError(DNotifierAgentError):
|
|
13
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='ToolExecutionError'
|
|
14
|
+
class PermissionDeniedError(DNotifierAgentError):
|
|
15
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='PermissionDeniedError'
|
|
16
|
+
class MCPConnectionError(DNotifierAgentError):
|
|
17
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='MCPConnectionError';A.retryable=False
|
|
18
|
+
class MCPToolError(DNotifierAgentError):
|
|
19
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='MCPToolError'
|
|
20
|
+
class AgentExecutionError(DNotifierAgentError):
|
|
21
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='AgentExecutionError'
|
|
22
|
+
class BackendError(DNotifierAgentError):
|
|
23
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='BackendError'
|
|
24
|
+
class LLMError(DNotifierAgentError):
|
|
25
|
+
def __init__(A,message,**B):super().__init__(message,**B);A.name='LLMError'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
def sanitize_id_part(value):A=re.sub('[^a-z0-9]+','_',str(value).strip().lower());return A.strip('_')
|
|
4
|
+
def local_tool_id(name):
|
|
5
|
+
A=sanitize_id_part(name)
|
|
6
|
+
if not A:raise ValueError('Tool name is required')
|
|
7
|
+
return f"tool_{A}"
|
|
8
|
+
def mcp_tool_id(server_id,tool_name):return f"mcp_{sanitize_id_part(server_id)}_{sanitize_id_part(tool_name)}"
|
|
9
|
+
def mcp_tool_name(server_id,tool_name):return f"{server_id}.{tool_name}"
|
|
10
|
+
def agent_backend_id(name):
|
|
11
|
+
A=sanitize_id_part(name)
|
|
12
|
+
if not A:raise ValueError('Agent name is required')
|
|
13
|
+
return f"agent_{A}"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_U='toOriginal'
|
|
3
|
+
_T='tool_call_id'
|
|
4
|
+
_S='toSanitized'
|
|
5
|
+
_R='calls'
|
|
6
|
+
_Q='toolCalls'
|
|
7
|
+
_P='choices'
|
|
8
|
+
_O='assistant'
|
|
9
|
+
_N='tool'
|
|
10
|
+
_M='id'
|
|
11
|
+
_L='tokensOut'
|
|
12
|
+
_K='tokensIn'
|
|
13
|
+
_J='arguments'
|
|
14
|
+
_I='type'
|
|
15
|
+
_H='function'
|
|
16
|
+
_G='tool_calls'
|
|
17
|
+
_F='message'
|
|
18
|
+
_E='role'
|
|
19
|
+
_D='model'
|
|
20
|
+
_C='name'
|
|
21
|
+
_B='content'
|
|
22
|
+
_A=None
|
|
23
|
+
import json,logging,re
|
|
24
|
+
from typing import Any,Optional,TYPE_CHECKING
|
|
25
|
+
from.errors import LLMError
|
|
26
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier
|
|
27
|
+
logger=logging.getLogger('dnotifier.agent_runtime')
|
|
28
|
+
def _extract_data_payload(response):
|
|
29
|
+
A=response
|
|
30
|
+
if not isinstance(A,dict):return
|
|
31
|
+
B=A.get('data')
|
|
32
|
+
if isinstance(B,dict)and not isinstance(B,list):return B
|
|
33
|
+
return A
|
|
34
|
+
def _extract_content(response):
|
|
35
|
+
D='text';A=response
|
|
36
|
+
if A is _A:return''
|
|
37
|
+
if isinstance(A,str):return A
|
|
38
|
+
B=_extract_data_payload(A);E=[B.get(_B)if B else _A,(B.get(_F)or{}).get(_B)if B else _A,B.get(D)if B else _A,((B.get(_P)or[{}])[0].get(_F)or{}).get(_B)if B else _A,A.get(_B)if isinstance(A,dict)else _A,(A.get(_F)or{}).get(_B)if isinstance(A,dict)else _A,A.get(D)if isinstance(A,dict)else _A]
|
|
39
|
+
for C in E:
|
|
40
|
+
if isinstance(C,str):return C
|
|
41
|
+
try:return json.dumps(A)
|
|
42
|
+
except(TypeError,ValueError):return str(A)
|
|
43
|
+
def _extract_assistant_message(response):
|
|
44
|
+
B=response
|
|
45
|
+
if not isinstance(B,dict):return
|
|
46
|
+
C=_extract_data_payload(B);D=[C,(C.get(_P)or[{}])[0].get(_F)if C else _A,C.get(_F)if C else _A,(B.get(_P)or[{}])[0].get(_F),B.get(_F),B]
|
|
47
|
+
for A in D:
|
|
48
|
+
if not isinstance(A,dict):continue
|
|
49
|
+
if isinstance(A.get(_G),list)or isinstance(A.get(_Q),list)or isinstance(A.get(_B),str)or _B in A and A.get(_B)is _A:return A
|
|
50
|
+
def _parse_tool_arguments(raw):
|
|
51
|
+
A=raw
|
|
52
|
+
if A is _A or A=='':return{}
|
|
53
|
+
if isinstance(A,dict):return A
|
|
54
|
+
if isinstance(A,str):
|
|
55
|
+
try:
|
|
56
|
+
B=json.loads(A)
|
|
57
|
+
if isinstance(B,dict):return B
|
|
58
|
+
return{}
|
|
59
|
+
except(TypeError,ValueError,json.JSONDecodeError):return{}
|
|
60
|
+
return{}
|
|
61
|
+
def parse_native_turn(response):
|
|
62
|
+
E=response;A=_extract_assistant_message(E);C=_A
|
|
63
|
+
if A:
|
|
64
|
+
C=A.get(_G)
|
|
65
|
+
if C is _A:C=A.get(_Q)
|
|
66
|
+
if isinstance(C,list)and len(C)>0:
|
|
67
|
+
F=[]
|
|
68
|
+
for(H,B)in enumerate(C):B=B or{};D=B.get(_H)if isinstance(B.get(_H),dict)else _A;F.append({_M:B.get(_M)or f"call_{H+1}",_C:(D or{}).get(_C)if D else B.get(_C),_J:_parse_tool_arguments((D or{}).get(_J)if D else B.get(_J))})
|
|
69
|
+
return{_I:_G,_R:F}
|
|
70
|
+
if A is not _A and _B in A:G=''if A.get(_B)is _A else str(A[_B])
|
|
71
|
+
else:G=_extract_content(E)
|
|
72
|
+
return{_I:_F,_B:G}
|
|
73
|
+
def _usage_from_response(response):
|
|
74
|
+
D='usage';C=response;B=_extract_data_payload(C);A=_A
|
|
75
|
+
if B:A=B.get(D)
|
|
76
|
+
if A is _A and isinstance(C,dict):A=C.get(D)
|
|
77
|
+
if not isinstance(A,dict):return{_K:0,_L:0,_D:str(B[_D])if B and B.get(_D)is not _A else _A}
|
|
78
|
+
return{_K:A.get('prompt_tokens')or A.get('input_tokens')or A.get(_K)or 0,_L:A.get('completion_tokens')or A.get('output_tokens')or A.get('total_tokens')or A.get(_L)or 0,_D:str(B[_D])if B and B.get(_D)is not _A else str(A[_D])if A.get(_D)is not _A else _A}
|
|
79
|
+
def to_openai_tool_name(name):A=re.sub('[^a-zA-Z0-9_-]','_',str(name or''));return A or _N
|
|
80
|
+
def build_openai_tool_name_map(tools=_A):
|
|
81
|
+
D={};C={}
|
|
82
|
+
for F in tools or[]:
|
|
83
|
+
A=str((F or{}).get(_C)or'')
|
|
84
|
+
if not A:continue
|
|
85
|
+
B=to_openai_tool_name(A);E=2
|
|
86
|
+
while B in C and C[B]!=A:B=f"{to_openai_tool_name(A)}_{E}";E+=1
|
|
87
|
+
D[A]=B;C[B]=A
|
|
88
|
+
return{_S:D,_U:C}
|
|
89
|
+
def to_openai_tools(tools=_A,to_sanitized=_A):
|
|
90
|
+
D='description';B=tools;E=to_sanitized or build_openai_tool_name_map(B)[_S];C=[]
|
|
91
|
+
for A in B or[]:C.append({_I:_H,_H:{_C:E.get(A[_C])or to_openai_tool_name(A[_C]),D:A.get(D)or'','parameters':A.get('inputSchema')or{_I:'object','properties':{}}}})
|
|
92
|
+
return C
|
|
93
|
+
def build_agent_chat_messages(history,to_sanitized=_A):
|
|
94
|
+
Q='user';L=to_sanitized;K=history;J='system';M=K if isinstance(K,list)else[];N='\n\n'.join(str(A.get(_B)or'').strip()for A in M if isinstance(A,dict)and A.get(_E)==J and str(A.get(_B)or'').strip());D=[]
|
|
95
|
+
if N:D.append({_E:J,_B:N})
|
|
96
|
+
for A in M:
|
|
97
|
+
if not isinstance(A,dict):continue
|
|
98
|
+
if A.get(_E)==J:continue
|
|
99
|
+
if A.get(_E)==Q:D.append({_E:Q,_B:str(A.get(_B)or'')});continue
|
|
100
|
+
if A.get(_E)==_O:
|
|
101
|
+
E=A.get(_Q)
|
|
102
|
+
if not isinstance(E,list):E=A.get(_G)
|
|
103
|
+
if isinstance(E,list)and len(E)>0:
|
|
104
|
+
I=A.get(_B);R=str(I)if I is not _A and str(I).strip()!=''else'';O=[]
|
|
105
|
+
for(S,B)in enumerate(E):
|
|
106
|
+
B=B or{};H=B.get(_H)if isinstance(B.get(_H),dict)else _A;C=(H or{}).get(_C)if H else B.get(_C);F=(H or{}).get(_J)if H else B.get(_J)
|
|
107
|
+
if not isinstance(F,str):F=json.dumps(F if F is not _A else{})
|
|
108
|
+
O.append({_M:str(B.get(_M)or f"call_{S+1}"),_I:_H,_H:{_C:((L or{}).get(C)if C else _A)or to_openai_tool_name(C),_J:F}})
|
|
109
|
+
D.append({_E:_O,_B:R,_G:O});continue
|
|
110
|
+
D.append({_E:_O,_B:str(A.get(_B)or'')});continue
|
|
111
|
+
if A.get(_E)==_N:
|
|
112
|
+
G=A.get('toolCallId')
|
|
113
|
+
if G is _A:G=A.get(_T)
|
|
114
|
+
if G is _A or str(G).strip()=='':continue
|
|
115
|
+
C=A.get(_C);P={_E:_N,_T:str(G),_B:str(A.get(_B)or'')}
|
|
116
|
+
if C:P[_C]=(L or{}).get(C)or to_openai_tool_name(C)
|
|
117
|
+
D.append(P)
|
|
118
|
+
return D
|
|
119
|
+
def describe_chat_message_roles(messages):
|
|
120
|
+
D=messages;B=[]
|
|
121
|
+
for(C,A)in enumerate(D if isinstance(D,list)else[]):
|
|
122
|
+
if not isinstance(A,dict):B.append(f"{C}:<?>");continue
|
|
123
|
+
if A.get(_E)==_O and isinstance(A.get(_G),list):B.append(f"{C}:assistant(tool_calls={len(A[_G])})");continue
|
|
124
|
+
if A.get(_E)==_N:B.append(f"{C}:tool(tool_call_id={A.get(_T)or'?'})");continue
|
|
125
|
+
B.append(f"{C}:{A.get(_E)or'?'}")
|
|
126
|
+
return' -> '.join(B)
|
|
127
|
+
async def complete_via_send_ai(notifier,body):
|
|
128
|
+
U=False;T='messages';L=notifier;K='tools';G='agentId';F='executionId';B='provider';A=body;M=A.get(K)if isinstance(A.get(K),list)else[];N=build_openai_tool_name_map(M);O=N[_S];V=N[_U];P=build_agent_chat_messages(A.get(T),O);Q=to_openai_tools(M,O);R=describe_chat_message_roles(P);S={'useKnowledgeBase':U,T:P}
|
|
129
|
+
if Q:S[K]=Q
|
|
130
|
+
try:C=await L.send_ai(sender_id=A.get('userId')or getattr(L,'user_id',_A),provider=A.get(B),model=A.get(_D),session_id=A.get('sessionId'),save_history=U,message=S)
|
|
131
|
+
except Exception as D:raise LLMError(str(D)or'Local sendAI completion failed',execution_id=A.get(F),agent_id=A.get(G),cause=D if isinstance(D,BaseException)else _A)from D
|
|
132
|
+
if C is _A:raise LLMError('Local sendAI completion returned empty response',execution_id=A.get(F),agent_id=A.get(G))
|
|
133
|
+
H=_extract_data_payload(C)
|
|
134
|
+
if H and H.get(_I)=='error':
|
|
135
|
+
I=H.get(_F)or'sendAI returned an error'
|
|
136
|
+
if re.search('role [\'\\"]tool[\'\\"].*tool_calls',str(I),re.I):logger.warning('[agent-runtime] sendAI rejected tool history. SDK messages: %s',R);logger.warning('[agent-runtime] If the digest shows assistant(tool_calls=N) immediately before tool(...), the server is likely dropping tool_calls before forwarding to the model.');raise LLMError(f"{I} (sdk messages: {R})",execution_id=A.get(F),agent_id=A.get(G))
|
|
137
|
+
raise LLMError(str(I),execution_id=A.get(F),agent_id=A.get(G))
|
|
138
|
+
J=_usage_from_response(C);W=str(A[_D])if A.get(_D)is not _A and str(A[_D]).strip()!=''else _A;X=str(A[B])if A.get(B)is not _A and str(A[B]).strip()!=''else _A;E=parse_native_turn(C)
|
|
139
|
+
if E[_I]==_G:E[_R]=[{**A,_C:V.get(A.get(_C),A.get(_C))}for A in E.get(_R)or[]]
|
|
140
|
+
return{**E,_D:J.get(_D)or W,B:X,_K:J[_K],_L:J[_L]}
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_J='message'
|
|
3
|
+
_I='params'
|
|
4
|
+
_H='2.0'
|
|
5
|
+
_G='method'
|
|
6
|
+
_F='jsonrpc'
|
|
7
|
+
_E=False
|
|
8
|
+
_D='error'
|
|
9
|
+
_C=True
|
|
10
|
+
_B='mcp'
|
|
11
|
+
_A=None
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any,Optional
|
|
14
|
+
import httpx
|
|
15
|
+
from..errors import MCPConnectionError,MCPToolError
|
|
16
|
+
PROTOCOL_VERSION='2025-03-26'
|
|
17
|
+
def _read_json(text):
|
|
18
|
+
if not text:return{}
|
|
19
|
+
try:return json.loads(text)
|
|
20
|
+
except(TypeError,ValueError,json.JSONDecodeError)as A:raise MCPConnectionError('MCP response was not JSON',cause=A)from A
|
|
21
|
+
def _read_sse_json(text):
|
|
22
|
+
A=[A[5:].strip()for A in text.split('\n')if A.startswith('data:')];A=[A for A in A if A]
|
|
23
|
+
if not A:return{}
|
|
24
|
+
C=A[-1]
|
|
25
|
+
try:return json.loads(C)
|
|
26
|
+
except(TypeError,ValueError,json.JSONDecodeError)as B:raise MCPConnectionError('MCP event stream was not JSON',provider=_B,cause=B)from B
|
|
27
|
+
def text_from_content(result):
|
|
28
|
+
B='text';A=result;C=A.get('content')if isinstance(A,dict)else _A;E=C if isinstance(C,list)else[];D='\n'.join(A.get(B)for A in E if isinstance(A,dict)and A.get('type')==B and isinstance(A.get(B),str))
|
|
29
|
+
if D:return D
|
|
30
|
+
try:return json.dumps(A if A is not _A else _A,default=str)
|
|
31
|
+
except(TypeError,ValueError):return str(A)
|
|
32
|
+
class McpHttpClient:
|
|
33
|
+
def __init__(A,*,endpoint,headers=_A,sdk_version='1.0.3'):A.endpoint=endpoint;A.headers=dict(headers or{});A.sdk_version=sdk_version;A.session_id=_A;A._next_id=1;A._initialized=_E
|
|
34
|
+
async def connect(A):await A._initialize()
|
|
35
|
+
async def list_tools(E):
|
|
36
|
+
D='tools';C=[];A=_A
|
|
37
|
+
while _C:
|
|
38
|
+
B=await E._request('tools/list',{'cursor':A}if A else{});F=B.get(D)if isinstance(B.get(D),list)else[];C.extend(F);A=B.get('nextCursor')or _A
|
|
39
|
+
if not A:break
|
|
40
|
+
return C
|
|
41
|
+
async def call_tool(C,name,args,context=_A):
|
|
42
|
+
B=context;B=B or{};A=await C._request('tools/call',{'name':name,'arguments':args if args is not _A else{}},B.get('signal'))
|
|
43
|
+
if isinstance(A,dict)and A.get('isError'):raise MCPToolError(text_from_content(A)or f"MCP tool failed: {name}",provider=_B)
|
|
44
|
+
return A
|
|
45
|
+
async def _initialize(A):
|
|
46
|
+
B=await A._post({_F:_H,'id':A._next_id,_G:'initialize',_I:{'protocolVersion':PROTOCOL_VERSION,'capabilities':{},'clientInfo':{'name':'dnotifier-python-sdk','version':A.sdk_version}}},_A);A._next_id+=1
|
|
47
|
+
if isinstance(B,dict)and B.get(_D):C=B[_D];D=(C.get(_J)if isinstance(C,dict)else _A)or'MCP initialize failed';raise MCPConnectionError(D,provider=_B)
|
|
48
|
+
A._initialized=_C;await A._post({_F:_H,_G:'notifications/initialized'},_A)
|
|
49
|
+
async def _request(A,method,params,signal=_A):
|
|
50
|
+
C=method
|
|
51
|
+
async def B():
|
|
52
|
+
F=A._next_id;A._next_id+=1;B=await A._post({_F:_H,'id':F,_G:C,_I:params},signal)
|
|
53
|
+
if isinstance(B,dict)and B.get(_D):D=B[_D];G=(D.get(_J)if isinstance(D,dict)else _A)or f"MCP {C} failed";raise MCPToolError(G,provider=_B)
|
|
54
|
+
E=B.get('result')if isinstance(B,dict)else _A;return E if isinstance(E,dict)else{}
|
|
55
|
+
try:return await B()
|
|
56
|
+
except Exception as D:
|
|
57
|
+
if not getattr(D,'retryable',_E):raise
|
|
58
|
+
A._initialized=_E;A.session_id=_A;await A._initialize();return await B()
|
|
59
|
+
async def _post(B,payload,signal):
|
|
60
|
+
H='mcp-session-id';G='content-type';E={'accept':'application/json, text/event-stream',G:'application/json',**B.headers}
|
|
61
|
+
if B.session_id:E[H]=B.session_id
|
|
62
|
+
try:
|
|
63
|
+
async with httpx.AsyncClient(timeout=6e1)as I:A=await I.post(B.endpoint,headers=E,content=json.dumps(payload))
|
|
64
|
+
except(MCPConnectionError,MCPToolError):raise
|
|
65
|
+
except Exception as D:C=MCPConnectionError(str(D)or'MCP connection failed',provider=_B,cause=D if isinstance(D,BaseException)else _A);C.retryable=_C;raise C from D
|
|
66
|
+
F=A.headers.get(H)
|
|
67
|
+
if F:B.session_id=F
|
|
68
|
+
if A.status_code==404:C=MCPConnectionError('MCP session expired',provider=_B);C.retryable=_C;raise C
|
|
69
|
+
if A.status_code>=400:J=A.text;raise MCPConnectionError(J or f"MCP HTTP {A.status_code}",provider=_B)
|
|
70
|
+
K=A.headers.get(G)or''
|
|
71
|
+
if'text/event-stream'in K:return _read_sse_json(A.text)
|
|
72
|
+
return _read_json(A.text)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_J='toolId'
|
|
3
|
+
_I='userId'
|
|
4
|
+
_H='agentId'
|
|
5
|
+
_G='executionId'
|
|
6
|
+
_F='transport'
|
|
7
|
+
_E='mcpServerId'
|
|
8
|
+
_D='name'
|
|
9
|
+
_C='mcp'
|
|
10
|
+
_B=None
|
|
11
|
+
_A='id'
|
|
12
|
+
from typing import Any,Optional,TYPE_CHECKING
|
|
13
|
+
from..errors import MCPConnectionError
|
|
14
|
+
from..ids import mcp_tool_id,mcp_tool_name
|
|
15
|
+
from.client import McpHttpClient,text_from_content
|
|
16
|
+
if TYPE_CHECKING:from..backend_client import AgentSync;from..observability import AgentObservability;from..tool_registry import ToolRegistry
|
|
17
|
+
class McpRegistry:
|
|
18
|
+
def __init__(A,*,tools,sync,events):A._tools=tools;A._sync=sync;A._events=events;A._connections={}
|
|
19
|
+
async def connect(B,config):
|
|
20
|
+
G='http';F='endpoint';A=config
|
|
21
|
+
if not A or not A.get(_A):raise MCPConnectionError('MCP connection id is required')
|
|
22
|
+
if not A.get(F):raise MCPConnectionError('MCP endpoint is required',provider=_C)
|
|
23
|
+
C=A.get(_F)or G
|
|
24
|
+
if C!=G:raise MCPConnectionError(f'MCP transport "{C}" is not implemented',provider=_C)
|
|
25
|
+
E=McpHttpClient(endpoint=A[F],headers=A.get('headers')or{});await E.connect();B._events.emit('mcp.connected',{_G:_B,_H:_B,_I:_B,_J:_B,_E:A[_A]});D=McpConnection(id=A[_A],name=A.get(_D)or A[_A],transport=C,client=E,tools=B._tools,sync=B._sync,events=B._events);await D.refresh_tools();B._connections[A[_A]]=D;return D
|
|
26
|
+
def get(A,connection_id):return A._connections.get(connection_id)
|
|
27
|
+
def list(A):return list(A._connections.values())
|
|
28
|
+
class McpConnection:
|
|
29
|
+
def __init__(A,*,id,name,transport,client,tools,sync,events):A.id=id;A.name=name;A.transport=transport;A._client=client;A._registry=tools;A._sync=sync;A._events=events;A._tools=[]
|
|
30
|
+
def tools(A):return A._tools
|
|
31
|
+
async def refresh_tools(A):
|
|
32
|
+
G='provider';F='inputSchema';E='description';H=await A._client.list_tools();A._tools=[]
|
|
33
|
+
for C in H:B=C.get(_D);D={_A:mcp_tool_id(A.id,B),_D:mcp_tool_name(A.id,B),E:C.get(E)or'',F:C.get(F)or{'type':'object'},G:_C,_E:A.id,'mcpToolName':B,'execute':lambda input_data,context,_name=B:A.call_tool(_name,input_data,context)};A._registry.adopt(D);A._events.emit('mcp.tool.discovered',{_G:_B,_H:_B,_I:_B,_J:D[_A],_E:A.id,G:_C});A._tools.append(D)
|
|
34
|
+
A._sync.enqueue_server({_A:A.id,_D:A.name,_F:A.transport},A._tools);return A._tools
|
|
35
|
+
async def call_tool(A,original_name,input_data,context=_B):B=await A._client.call_tool(original_name,input_data,context or{});return text_from_content(B)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from datetime import datetime,timezone
|
|
3
|
+
from typing import Any,Callable,Optional,TYPE_CHECKING
|
|
4
|
+
from..config.endpoints import DNotifierEndpoints
|
|
5
|
+
if TYPE_CHECKING:from.backend_client import AgentBackendClient
|
|
6
|
+
class AgentObservability:
|
|
7
|
+
def __init__(A,*,backend,on_agent_event=None):A.backend=backend;A.on_agent_event=on_agent_event
|
|
8
|
+
def emit(A,event_type,fields):
|
|
9
|
+
B={'type':event_type,**fields,'at':datetime.now(timezone.utc).isoformat()}
|
|
10
|
+
try:
|
|
11
|
+
if A.on_agent_event:A.on_agent_event(B)
|
|
12
|
+
except Exception:pass
|
|
13
|
+
try:import asyncio as C;D=C.get_running_loop();D.create_task(A.backend.post_quiet(DNotifierEndpoints.agent_events_url,B))
|
|
14
|
+
except RuntimeError:pass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
from typing import Any,Literal,Optional,TYPE_CHECKING
|
|
4
|
+
from.errors import PermissionDeniedError
|
|
5
|
+
if TYPE_CHECKING:from.backend_client import AgentBackendClient
|
|
6
|
+
async def assert_tool_allowed(*,backend,enforcement,execution_id,agent_id,tool_id,user_id=_A,provider=_A):
|
|
7
|
+
E=tool_id;D=agent_id;C=execution_id
|
|
8
|
+
if enforcement=='disabled':return
|
|
9
|
+
F={'execution_id':C,'agent_id':D,'tool_id':E,'provider':provider}
|
|
10
|
+
try:A=await backend.check_permission({'executionId':C,'agentId':D,'userId':user_id,'toolId':E,'action':'execute'})
|
|
11
|
+
except Exception as B:raise PermissionDeniedError('Permission check failed closed',**F,cause=B if isinstance(B,BaseException)else _A)from B
|
|
12
|
+
if not isinstance(A,dict)or A.get('decision')!='allow':G=(A.get('reason')if isinstance(A,dict)else _A)or'Permission denied';raise PermissionDeniedError(G,**F)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_p='toolCallId'
|
|
3
|
+
_o='byName'
|
|
4
|
+
_n='notifier'
|
|
5
|
+
_m='lastStepId'
|
|
6
|
+
_l='arguments'
|
|
7
|
+
_k='call'
|
|
8
|
+
_j='agentName'
|
|
9
|
+
_i='messages'
|
|
10
|
+
_h='tool'
|
|
11
|
+
_g='metadata'
|
|
12
|
+
_f='agentId'
|
|
13
|
+
_e='observability'
|
|
14
|
+
_d='tool_calls'
|
|
15
|
+
_c='sendAI'
|
|
16
|
+
_b='signal'
|
|
17
|
+
_a='services'
|
|
18
|
+
_Z='sessionId'
|
|
19
|
+
_Y='toolId'
|
|
20
|
+
_X='state'
|
|
21
|
+
_W='latencyMs'
|
|
22
|
+
_V='status'
|
|
23
|
+
_U='label'
|
|
24
|
+
_T='apiName'
|
|
25
|
+
_S='kind'
|
|
26
|
+
_R='trace'
|
|
27
|
+
_Q='round'
|
|
28
|
+
_P='output'
|
|
29
|
+
_O='userId'
|
|
30
|
+
_N='role'
|
|
31
|
+
_M='siblingIndex'
|
|
32
|
+
_L='input'
|
|
33
|
+
_K='parentStepId'
|
|
34
|
+
_J='message'
|
|
35
|
+
_I='mcpServerId'
|
|
36
|
+
_H='content'
|
|
37
|
+
_G='executionId'
|
|
38
|
+
_F='error'
|
|
39
|
+
_E='name'
|
|
40
|
+
_D='type'
|
|
41
|
+
_C='id'
|
|
42
|
+
_B='provider'
|
|
43
|
+
_A=None
|
|
44
|
+
import json,time
|
|
45
|
+
from typing import Any,Optional,TYPE_CHECKING
|
|
46
|
+
from..workflow.ids import short_id
|
|
47
|
+
from..workflow.step_types import STEP_TYPE_TOOL,to_log_step_type
|
|
48
|
+
from.descriptors import to_public_tool
|
|
49
|
+
from.errors import AgentExecutionError,DNotifierAgentError,LLMError,PermissionDeniedError,ToolExecutionError,ToolNotFoundError
|
|
50
|
+
from.ids import agent_backend_id
|
|
51
|
+
from.local_completions import build_agent_chat_messages,build_openai_tool_name_map,complete_via_send_ai,to_openai_tools
|
|
52
|
+
from.permissions import assert_tool_allowed
|
|
53
|
+
from.schema import assert_valid_input
|
|
54
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier;from..workflow.agent import Agent;from..workflow.context import WorkflowContext
|
|
55
|
+
STEP_TYPE_AI_LLM=to_log_step_type(_c)
|
|
56
|
+
TRACE_CONTENT_MAX=8000
|
|
57
|
+
def _snapshot(value):
|
|
58
|
+
A=value
|
|
59
|
+
try:
|
|
60
|
+
if A is _A:return
|
|
61
|
+
return json.loads(json.dumps(A,default=str))
|
|
62
|
+
except(TypeError,ValueError):return str(A)
|
|
63
|
+
def _truncate_text(value,max_len=TRACE_CONTENT_MAX):
|
|
64
|
+
C=value;B=max_len;A=''if C is _A else str(C)
|
|
65
|
+
if len(A)<=B:return A
|
|
66
|
+
return f"{A[:B]}…[truncated {len(A)-B} chars]"
|
|
67
|
+
def _trace_llm_input(*,round_num,messages,tools):B='tool_call_id';A=[to_public_tool(A)for A in tools];C=build_openai_tool_name_map(A)['toSanitized'];D=build_agent_chat_messages(messages,C);E=to_openai_tools(A,C);return _snapshot({_Q:round_num,'messageCount':len(D),_i:[{**{_N:A[_N]},**({_H:_truncate_text(A[_H])}if A.get(_H)is not _A else{}),**({_d:A[_d]}if isinstance(A.get(_d),list)else{}),**({B:A[B]}if A.get(B)is not _A else{})}for A in D],'tools':E})
|
|
68
|
+
def _initial_messages(input_data,instructions):
|
|
69
|
+
E='text';D=instructions;C='user';A=input_data;B=[]
|
|
70
|
+
if D:B.append({_N:'system',_H:D})
|
|
71
|
+
if isinstance(A,str):B.append({_N:C,_H:A});return B
|
|
72
|
+
if isinstance(A,dict):
|
|
73
|
+
if isinstance(A.get(_i),list):B.extend(A[_i]);return B
|
|
74
|
+
if isinstance(A.get(E),str):B.append({_N:C,_H:A[E]});return B
|
|
75
|
+
if isinstance(A.get(_J),str):B.append({_N:C,_H:A[_J]});return B
|
|
76
|
+
B.append({_N:C,_H:''if A is _A else json.dumps(A,default=str)});return B
|
|
77
|
+
def _result_text(value):
|
|
78
|
+
A=value
|
|
79
|
+
if isinstance(A,str):return A
|
|
80
|
+
try:return json.dumps(A if A is not _A else _A,default=str)
|
|
81
|
+
except(TypeError,ValueError):return str(A)
|
|
82
|
+
def _parse_arguments(raw,fields):
|
|
83
|
+
B=fields;A=raw
|
|
84
|
+
if A is _A or A=='':return{}
|
|
85
|
+
if isinstance(A,str):
|
|
86
|
+
try:return json.loads(A)
|
|
87
|
+
except(TypeError,ValueError,json.JSONDecodeError)as C:raise ToolExecutionError('Tool arguments are not valid JSON',**B,cause=C)from C
|
|
88
|
+
if isinstance(A,dict):return A
|
|
89
|
+
raise ToolExecutionError('Tool arguments must be an object',**B)
|
|
90
|
+
def workflow_trace_from_context(ctx):
|
|
91
|
+
A=ctx
|
|
92
|
+
if not getattr(A,'_observability_enabled',False)or not getattr(A,'_observability',_A)or not getattr(A,'_execution_id',_A):return
|
|
93
|
+
return{_e:A._observability,_K:A._current_step_id or A._trace_parent_step_id,_j:A.agent_name}
|
|
94
|
+
async def run_agent_loop(params):
|
|
95
|
+
k='Completion did not return a message or tool call';j='stop';f='calls';b='%Y-%m-%dT%H:%M:%S.000Z';a='startedAt';Z='llmLatencyMs';Q='finishReason';P='tokensOut';O='tokensIn';H='model';B=params;U=B[_n];C=B['agent'];S=agent_backend_id(C.name);D=B.get(_G)or short_id('exec');V=B.get(_O)
|
|
96
|
+
if V is _A:V=getattr(U,'user_id',_A)
|
|
97
|
+
g=B.get(_Z);h=B.get('maxToolRounds')or 8;I=B.get(_R)
|
|
98
|
+
if not getattr(U,'is_connected',False):raise AgentExecutionError('Not connected',execution_id=D,agent_id=S)
|
|
99
|
+
c=U._agent_services;c.sync.enqueue_agent(C);await c.sync.flush();d=C.tools or[];l={A[_E]:A for A in d};W=_initial_messages(B.get(_L),C.instructions or C.description or'');K=B.get(_B)
|
|
100
|
+
if K is _A:K=C.provider
|
|
101
|
+
L=B.get(H)
|
|
102
|
+
if L is _A:L=C.model
|
|
103
|
+
m=set();n=(I or{}).get(_K)if I else _A;M=n;T=_A;F=_A
|
|
104
|
+
for R in range(h):
|
|
105
|
+
J=int(time.time()*1000);E=_allocate_trace_step_id(I);X=_trace_llm_input(round_num=R,messages=W,tools=d)
|
|
106
|
+
try:A=await complete_via_send_ai(U,{_G:D,_f:S,_O:V,_Z:g,_i:W,'tools':[to_public_tool(A)for A in d],_B:K,H:L})
|
|
107
|
+
except Exception as o:G=int(time.time()*1000)-J;await _record_trace_step({_G:D,_R:I,_K:M,_M:T},{_C:E,_D:STEP_TYPE_AI_LLM,_S:C.name,_T:_c,_U:STEP_TYPE_AI_LLM,_V:_F,_W:G,H:L,_B:K,O:0,P:0,_L:X,_P:_A,_X:_snapshot({_Q:R,Q:_F,Z:G,a:time.strftime(b,time.gmtime(J/1000))}),_F:{_J:str(o)}});F=E or F;raise
|
|
108
|
+
Y=j if A[_D]==_J else _d;N=_A
|
|
109
|
+
if A[_D]==_d:
|
|
110
|
+
i=A.get(f)if isinstance(A.get(f),list)else[]
|
|
111
|
+
if i:N=[_ensure_unique_tool_call(A,m)for A in i]
|
|
112
|
+
if A[_D]!=_J and not N:G=int(time.time()*1000)-J;await _record_trace_step({_G:D,_R:I,_K:M,_M:T},{_C:E,_D:STEP_TYPE_AI_LLM,_S:C.name,_T:_c,_U:STEP_TYPE_AI_LLM,_V:_F,_W:G,H:A.get(H)or L,_B:A.get(_B)or K,O:A.get(O)or 0,P:A.get(P)or 0,_L:X,_P:_snapshot(A),_X:_snapshot({_Q:R,Q:_F,Z:G,a:time.strftime(b,time.gmtime(J/1000))}),_F:{_J:k}});F=E or F;raise LLMError(k,execution_id=D,agent_id=S)
|
|
113
|
+
G=int(time.time()*1000)-J
|
|
114
|
+
if A[_D]==_J:await _record_trace_step({_G:D,_R:I,_K:M,_M:T},{_C:E,_D:STEP_TYPE_AI_LLM,_S:C.name,_T:_c,_U:STEP_TYPE_AI_LLM,_V:'ok',_W:G,H:A.get(H)or L,_B:A.get(_B)or K,O:A.get(O)or 0,P:A.get(P)or 0,_L:X,_P:_snapshot({_D:A[_D],_H:_truncate_text(A.get(_H)or''),Q:Y}),_X:_snapshot({_Q:R,Q:Y,Z:G,a:time.strftime(b,time.gmtime(J/1000))}),_F:_A});F=E or F;return{_P:A.get(_H)or'',_G:D,Q:j,_m:F}
|
|
115
|
+
assert N is not _A;W.append({_N:'assistant','toolCalls':N});p=E or M
|
|
116
|
+
for(q,e)in enumerate(N):r=await _execute_tool_call({_a:c,_f:S,_G:D,_O:V,_Z:g,_b:B.get(_b),_g:B.get(_g),_o:l,_k:e,_R:I,_K:p,_M:q,_Q:R});W.append({_N:_h,_p:e.get(_C),_E:e.get(_E),_H:r})
|
|
117
|
+
s=int(time.time()*1000)-J;await _record_trace_step({_G:D,_R:I,_K:M,_M:T},{_C:E,_D:STEP_TYPE_AI_LLM,_S:C.name,_T:_c,_U:STEP_TYPE_AI_LLM,_V:'ok',_W:s,H:A.get(H)or L,_B:A.get(_B)or K,O:A.get(O)or 0,P:A.get(P)or 0,_L:X,_P:_snapshot({_D:A[_D],f:[{_C:A.get(_C),_E:A.get(_E),_l:A.get(_l)or{},_M:B}for(B,A)in enumerate(N)],Q:Y}),_X:_snapshot({_Q:R,Q:Y,Z:G,a:time.strftime(b,time.gmtime(J/1000))}),_F:_A});F=E or F;M=E or M;T=len(N)
|
|
118
|
+
raise AgentExecutionError(f"Agent stopped after {h} tool rounds",execution_id=D,agent_id=S)
|
|
119
|
+
async def run_tool_agent(agent,ctx):
|
|
120
|
+
A=ctx;B=await run_agent_loop({_n:A.notifier,'agent':agent,_L:A.input,_G:A._execution_id,_O:A.sender_id,_Z:_A,_b:getattr(A,_b,_A),_g:_A,_R:workflow_trace_from_context(A)})
|
|
121
|
+
if B and B.get(_m):A._last_edge_step_id=B[_m]
|
|
122
|
+
return B
|
|
123
|
+
def _allocate_trace_step_id(trace):
|
|
124
|
+
A=trace
|
|
125
|
+
if not A or not A.get(_e):return
|
|
126
|
+
C=A[_e];B=getattr(C,'allocate_step_id',_A)
|
|
127
|
+
if not callable(B):return
|
|
128
|
+
try:return B()
|
|
129
|
+
except Exception:return
|
|
130
|
+
def _ensure_unique_tool_call(call,used_ids):
|
|
131
|
+
C=used_ids;D=call.get(_C);B=str(D).strip()if D is not _A else'';A=B if B and B not in C else _A
|
|
132
|
+
if not A:A=short_id(_k)
|
|
133
|
+
C.add(A);return{**call,_C:A}
|
|
134
|
+
async def _record_trace_step(params,step):
|
|
135
|
+
B=step;A=params;C=A.get(_R)
|
|
136
|
+
if not C or not C.get(_e)or not A.get(_G):return B.get(_C)
|
|
137
|
+
try:
|
|
138
|
+
E=A[_K]if _K in A else C.get(_K);F=A[_M]if _M in A else B.get(_M);D=await C[_e].record_step(A[_G],{**B,_K:E,_j:B.get(_j)or C.get(_j),_M:F})
|
|
139
|
+
if isinstance(D,dict):return D.get(_C)or B.get(_C)
|
|
140
|
+
return B.get(_C)
|
|
141
|
+
except Exception:return B.get(_C)
|
|
142
|
+
def _tool_trace_state(params,extra=_A):B=params;A=extra;C=B.get(_k)or{};A=A or{};return _snapshot({_Y:A.get(_Y),_B:A.get(_B),_I:A.get(_I),_p:C.get(_C),_Q:B.get(_Q),**A})
|
|
143
|
+
async def _execute_tool_call(params):
|
|
144
|
+
P='tool.failed';O='tool.completed';N='tool.started';K='mcp';B=params;F=B[_k];Q=B[_o];G=B[_G];H=B[_f];A=Q.get(F.get(_E));I={'execution_id':G,'agent_id':H,'tool_id':(A or{}).get(_C),_B:(A or{}).get(_B)}
|
|
145
|
+
if not A:raise ToolNotFoundError(f"Tool not found: {F.get(_E)or''}",**I)
|
|
146
|
+
try:D=_parse_arguments(F.get(_l),I);assert_valid_input(A.get('inputSchema'),D,I)
|
|
147
|
+
except Exception as C:await _record_trace_step(B,{_D:STEP_TYPE_TOOL,_S:A[_E],_T:_h,_U:A[_E],_V:_F,_W:0,_B:A.get(_B),_L:_snapshot(F.get(_l)),_P:_A,_F:{_J:str(C)},_X:_tool_trace_state(B,{_Y:A[_C],_B:A.get(_B),_I:A.get(_I)})});raise
|
|
148
|
+
try:await assert_tool_allowed(backend=B[_a].backend,enforcement=B[_a].permission_enforcement,execution_id=G,agent_id=H,user_id=B.get(_O),tool_id=A[_C],provider=A.get(_B))
|
|
149
|
+
except Exception as C:await _record_trace_step(B,{_D:STEP_TYPE_TOOL,_S:A[_E],_T:_h,_U:A[_E],_V:_F,_W:0,_B:A.get(_B),_L:_snapshot(D),_P:_A,_F:{_J:str(C),**({'code':'permission_denied'}if isinstance(C,PermissionDeniedError)else{})},_X:_tool_trace_state(B,{_Y:A[_C],_B:A.get(_B),_I:A.get(_I),'decision':'deny'})});raise
|
|
150
|
+
R={_G:G,_f:H,_O:B.get(_O),_Z:B.get(_Z),_Y:A[_C],_g:B.get(_g),_b:B.get(_b)};J={_G:G,_f:H,_O:B.get(_O),_Y:A[_C],_I:A.get(_I),_B:A.get(_B)};S=[N,'mcp.tool.started']if A.get(_B)==K else[N]
|
|
151
|
+
for E in S:B[_a].events.emit(E,J)
|
|
152
|
+
L=int(time.time()*1000)
|
|
153
|
+
try:
|
|
154
|
+
M=await A['execute'](D,R);T=_result_text(M);U=[O,'mcp.tool.completed']if A.get(_B)==K else[O]
|
|
155
|
+
for E in U:B[_a].events.emit(E,J)
|
|
156
|
+
await _record_trace_step(B,{_D:STEP_TYPE_TOOL,_S:A[_E],_T:_h,_U:A[_E],_V:'ok',_W:int(time.time()*1000)-L,_B:A.get(_B),_L:_snapshot(D),_P:_snapshot(M),_F:_A,_X:_tool_trace_state(B,{_Y:A[_C],_B:A.get(_B),_I:A.get(_I)})});return T
|
|
157
|
+
except Exception as C:
|
|
158
|
+
V=[P,'mcp.tool.failed']if A.get(_B)==K else[P]
|
|
159
|
+
for E in V:B[_a].events.emit(E,{**J,_J:str(C)})
|
|
160
|
+
await _record_trace_step(B,{_D:STEP_TYPE_TOOL,_S:A[_E],_T:_h,_U:A[_E],_V:_F,_W:int(time.time()*1000)-L,_B:A.get(_B),_L:_snapshot(D),_P:_A,_F:{_J:str(C),'stack':getattr(C,'__traceback__',_A)},_X:_tool_trace_state(B,{_Y:A[_C],_B:A.get(_B),_I:A.get(_I)})})
|
|
161
|
+
if isinstance(C,DNotifierAgentError):raise
|
|
162
|
+
raise ToolExecutionError(str(C)or'Tool execution failed',**I,cause=C if isinstance(C,BaseException)else _A)from C
|