newbro-cli 0.1.0__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.
- newbro_cli-0.1.0.dist-info/METADATA +333 -0
- newbro_cli-0.1.0.dist-info/RECORD +186 -0
- newbro_cli-0.1.0.dist-info/WHEEL +5 -0
- newbro_cli-0.1.0.dist-info/entry_points.txt +2 -0
- newbro_cli-0.1.0.dist-info/top_level.txt +1 -0
- synapse/__init__.py +3 -0
- synapse/__main__.py +9 -0
- synapse/api/__init__.py +1 -0
- synapse/api/app.py +54 -0
- synapse/api/logging.py +42 -0
- synapse/api/models.py +135 -0
- synapse/api/paths.py +26 -0
- synapse/api/routes/__init__.py +1 -0
- synapse/api/routes/commands.py +66 -0
- synapse/api/routes/executor_nodes.py +125 -0
- synapse/api/routes/health.py +8 -0
- synapse/api/routes/interaction_requests.py +59 -0
- synapse/api/routes/messages.py +46 -0
- synapse/api/routes/personas.py +124 -0
- synapse/api/routes/session_config.py +64 -0
- synapse/api/routes/sessions.py +129 -0
- synapse/api/ws/__init__.py +1 -0
- synapse/api/ws/executors.py +69 -0
- synapse/api/ws/stream.py +363 -0
- synapse/blackboard/__init__.py +14 -0
- synapse/blackboard/backends/__init__.py +5 -0
- synapse/blackboard/backends/memory.py +491 -0
- synapse/blackboard/interfaces.py +167 -0
- synapse/blackboard/queries.py +31 -0
- synapse/blackboard/revisions.py +30 -0
- synapse/blackboard/store.py +31 -0
- synapse/blackboard/subscriptions.py +23 -0
- synapse/cli/__init__.py +5 -0
- synapse/cli/main.py +1958 -0
- synapse/communication/__init__.py +6 -0
- synapse/communication/brain.py +1274 -0
- synapse/communication/context.py +219 -0
- synapse/communication/history.py +80 -0
- synapse/communication/model.py +107 -0
- synapse/communication/models/__init__.py +4 -0
- synapse/communication/models/openai.py +331 -0
- synapse/communication/models/scripted.py +167 -0
- synapse/communication/persona_pool.py +200 -0
- synapse/communication/policies/__init__.py +4 -0
- synapse/communication/policies/reply_style.py +59 -0
- synapse/communication/policies/tool_usage_policy.py +10 -0
- synapse/communication/prompts/__init__.py +3 -0
- synapse/communication/prompts/base/__init__.py +1 -0
- synapse/communication/prompts/base/guardrails.py +6 -0
- synapse/communication/prompts/base/identity.py +7 -0
- synapse/communication/prompts/base/persona_identity.py +21 -0
- synapse/communication/prompts/base/reply_style.py +6 -0
- synapse/communication/prompts/base/tool_policy.py +57 -0
- synapse/communication/prompts/builders.py +168 -0
- synapse/communication/prompts/examples/__init__.py +1 -0
- synapse/communication/prompts/examples/notification_style.py +11 -0
- synapse/communication/prompts/examples/tool_usage.py +60 -0
- synapse/communication/prompts/runtime_context.py +199 -0
- synapse/communication/prompts/tasks/__init__.py +1 -0
- synapse/communication/prompts/tasks/normal_reply.py +40 -0
- synapse/communication/prompts/tasks/proactive_notification.py +14 -0
- synapse/communication/resolver.py +156 -0
- synapse/communication/tools/__init__.py +256 -0
- synapse/communication/tools/add_constraint.py +63 -0
- synapse/communication/tools/add_task_note.py +59 -0
- synapse/communication/tools/base.py +22 -0
- synapse/communication/tools/control_task.py +82 -0
- synapse/communication/tools/create_task.py +127 -0
- synapse/communication/tools/list_tasks.py +53 -0
- synapse/communication/tools/query_task_detail.py +57 -0
- synapse/communication/tools/query_task_summary.py +34 -0
- synapse/communication/tools/resolve_interaction_request.py +67 -0
- synapse/communication/tools/update_task.py +109 -0
- synapse/communication/types.py +22 -0
- synapse/config_home.py +72 -0
- synapse/connectors/__init__.py +1 -0
- synapse/connectors/base/__init__.py +20 -0
- synapse/connectors/base/bindings.py +145 -0
- synapse/connectors/base/module.py +48 -0
- synapse/connectors/base/transport.py +202 -0
- synapse/connectors/host/__init__.py +17 -0
- synapse/connectors/host/app.py +60 -0
- synapse/connectors/host/catalog.py +29 -0
- synapse/connectors/host/config.py +164 -0
- synapse/connectors/host/registry.py +21 -0
- synapse/connectors/voice/__init__.py +1 -0
- synapse/connectors/voice/agora_convoai/__init__.py +20 -0
- synapse/connectors/voice/agora_convoai/app.py +4 -0
- synapse/connectors/voice/agora_convoai/models.py +116 -0
- synapse/connectors/voice/agora_convoai/module.py +391 -0
- synapse/connectors/voice/agora_convoai/service.py +513 -0
- synapse/connectors/voice/agora_convoai/session_service.py +286 -0
- synapse/connectors/voice/agora_convoai/settings.py +223 -0
- synapse/connectors/voice/agora_convoai/token_utils.py +153 -0
- synapse/envfile.py +25 -0
- synapse/execution/__init__.py +21 -0
- synapse/execution/assignment.py +53 -0
- synapse/execution/brain.py +45 -0
- synapse/execution/mode_manager.py +105 -0
- synapse/execution/reconcile.py +178 -0
- synapse/execution/run_manager.py +184 -0
- synapse/execution/scheduler.py +13 -0
- synapse/execution/session_manager.py +122 -0
- synapse/execution/summary_manager.py +84 -0
- synapse/executors/__init__.py +1 -0
- synapse/executors/adapters/__init__.py +17 -0
- synapse/executors/adapters/acpx/__init__.py +4 -0
- synapse/executors/adapters/acpx/executor.py +610 -0
- synapse/executors/adapters/acpx/session.py +117 -0
- synapse/executors/adapters/codex/__init__.py +4 -0
- synapse/executors/adapters/codex/client.py +182 -0
- synapse/executors/adapters/codex/executor.py +455 -0
- synapse/executors/adapters/codex/jsonrpc.py +108 -0
- synapse/executors/adapters/codex/session.py +124 -0
- synapse/executors/adapters/hosted/__init__.py +3 -0
- synapse/executors/adapters/hosted/executor.py +95 -0
- synapse/executors/adapters/mock/__init__.py +5 -0
- synapse/executors/adapters/mock/config.py +7 -0
- synapse/executors/adapters/mock/executor.py +91 -0
- synapse/executors/adapters/mock/session.py +7 -0
- synapse/executors/core/__init__.py +19 -0
- synapse/executors/core/capabilities.py +12 -0
- synapse/executors/core/events.py +22 -0
- synapse/executors/core/executor.py +32 -0
- synapse/executors/core/registry.py +28 -0
- synapse/executors/core/results.py +12 -0
- synapse/executors/core/session.py +12 -0
- synapse/executors/node/__init__.py +15 -0
- synapse/executors/node/__main__.py +43 -0
- synapse/executors/node/config.py +130 -0
- synapse/executors/node/registry.py +310 -0
- synapse/executors/node/service.py +397 -0
- synapse/infrastructure/__init__.py +1 -0
- synapse/infrastructure/llm/__init__.py +3 -0
- synapse/infrastructure/llm/openai_provider.py +454 -0
- synapse/interaction/__init__.py +3 -0
- synapse/interaction/manager.py +353 -0
- synapse/interaction/sanitization.py +76 -0
- synapse/notification/__init__.py +14 -0
- synapse/notification/candidate_builder.py +136 -0
- synapse/notification/manager.py +179 -0
- synapse/notification/policy.py +91 -0
- synapse/observability/__init__.py +25 -0
- synapse/observability/bootstrap.py +146 -0
- synapse/observability/context.py +47 -0
- synapse/observability/emitters/__init__.py +29 -0
- synapse/observability/emitters/api.py +87 -0
- synapse/observability/emitters/blackboard.py +76 -0
- synapse/observability/emitters/communication.py +178 -0
- synapse/observability/emitters/execution.py +154 -0
- synapse/observability/emitters/notification.py +111 -0
- synapse/observability/logger.py +73 -0
- synapse/observability/reason_codes.py +14 -0
- synapse/observability/redaction.py +117 -0
- synapse/observability/schema.py +57 -0
- synapse/observability/sinks/__init__.py +4 -0
- synapse/observability/sinks/pretty.py +216 -0
- synapse/observability/sinks/stdout.py +16 -0
- synapse/observability/sinks/types.py +10 -0
- synapse/observability/store.py +85 -0
- synapse/protocol/__init__.py +97 -0
- synapse/protocol/assignment.py +9 -0
- synapse/protocol/command.py +14 -0
- synapse/protocol/enums.py +138 -0
- synapse/protocol/execution_mode.py +12 -0
- synapse/protocol/executor_node.py +120 -0
- synapse/protocol/interaction.py +47 -0
- synapse/protocol/interruption.py +13 -0
- synapse/protocol/mutation.py +16 -0
- synapse/protocol/notification.py +22 -0
- synapse/protocol/persona.py +19 -0
- synapse/protocol/run.py +20 -0
- synapse/protocol/session.py +43 -0
- synapse/protocol/summary.py +11 -0
- synapse/protocol/task.py +22 -0
- synapse/protocol/task_execution_detail.py +14 -0
- synapse/runtime/__init__.py +28 -0
- synapse/runtime/bootstrap.py +29 -0
- synapse/runtime/config.py +209 -0
- synapse/runtime/container.py +102 -0
- synapse/runtime/executor_node_manager.py +477 -0
- synapse/runtime/models.py +112 -0
- synapse/runtime/session.py +1212 -0
- synapse/service/__init__.py +3 -0
- synapse/service/app.py +96 -0
- synapse/yaml_support.py +136 -0
synapse/api/models.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from synapse.observability.schema import DiagnosticEvent
|
|
8
|
+
from synapse.protocol import ExecutorNodeCredentialIssue, ExecutorNodeRecord, TaskCommandType
|
|
9
|
+
from synapse.runtime.models import SessionSnapshot
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SessionResponse(BaseModel):
|
|
13
|
+
session_id: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MessageRequest(BaseModel):
|
|
17
|
+
text: str
|
|
18
|
+
source: Literal["user", "connector"] = "user"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolInvocationSummary(BaseModel):
|
|
22
|
+
tool_name: str
|
|
23
|
+
args: dict[str, object] = Field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MessageResponse(BaseModel):
|
|
27
|
+
message_id: str
|
|
28
|
+
reply_text: str
|
|
29
|
+
conversational_act: str
|
|
30
|
+
affected_task_ids: list[str] = Field(default_factory=list)
|
|
31
|
+
tool_invocations: list[ToolInvocationSummary] = Field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CommandRequest(BaseModel):
|
|
35
|
+
command_type: TaskCommandType
|
|
36
|
+
task_id: str | None = None
|
|
37
|
+
reference: str | None = None
|
|
38
|
+
payload: dict[str, object] = Field(default_factory=dict)
|
|
39
|
+
reason: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CommandResponse(BaseModel):
|
|
43
|
+
command_id: str
|
|
44
|
+
status: str = "accepted"
|
|
45
|
+
affected_task_ids: list[str] = Field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ResolveInteractionRequest(BaseModel):
|
|
49
|
+
action: Literal["approve", "deny", "answer", "confirm", "cancel"]
|
|
50
|
+
answer_text: str | None = None
|
|
51
|
+
option_id: str | None = None
|
|
52
|
+
reason: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ResolveInteractionRequestResponse(BaseModel):
|
|
56
|
+
request_id: str
|
|
57
|
+
status: str = "accepted"
|
|
58
|
+
affected_task_ids: list[str] = Field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SendMessageSocketAction(BaseModel):
|
|
62
|
+
type: Literal["send_message"] = "send_message"
|
|
63
|
+
request_id: str
|
|
64
|
+
text: str
|
|
65
|
+
source: Literal["user", "connector"] = "user"
|
|
66
|
+
target_persona_id: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SendCommandSocketAction(BaseModel):
|
|
70
|
+
type: Literal["send_command"] = "send_command"
|
|
71
|
+
request_id: str
|
|
72
|
+
command_type: TaskCommandType
|
|
73
|
+
task_id: str | None = None
|
|
74
|
+
reference: str | None = None
|
|
75
|
+
payload: dict[str, object] = Field(default_factory=dict)
|
|
76
|
+
reason: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ResolveInteractionRequestSocketAction(BaseModel):
|
|
80
|
+
type: Literal["resolve_interaction_request"] = "resolve_interaction_request"
|
|
81
|
+
request_id: str
|
|
82
|
+
interaction_request_id: str
|
|
83
|
+
action: Literal["approve", "deny", "answer", "confirm", "cancel"]
|
|
84
|
+
answer_text: str | None = None
|
|
85
|
+
option_id: str | None = None
|
|
86
|
+
reason: str | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DiagnosticTimelineResponse(BaseModel):
|
|
90
|
+
events: list[DiagnosticEvent] = Field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PersonaCreateRequest(BaseModel):
|
|
94
|
+
name: str
|
|
95
|
+
avatar: str = ""
|
|
96
|
+
base_prompt: str = ""
|
|
97
|
+
executor_node_id: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PersonaUpdateRequest(BaseModel):
|
|
101
|
+
name: str | None = None
|
|
102
|
+
avatar: str | None = None
|
|
103
|
+
base_prompt: str | None = None
|
|
104
|
+
executor_node_id: str | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ExecutorNodeCreateRequest(BaseModel):
|
|
108
|
+
name: str
|
|
109
|
+
enabled_executors: list[str] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ExecutorNodeUpdateRequest(BaseModel):
|
|
113
|
+
name: str | None = None
|
|
114
|
+
enabled_executors: list[str] | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"CommandRequest",
|
|
119
|
+
"CommandResponse",
|
|
120
|
+
"DiagnosticTimelineResponse",
|
|
121
|
+
"ExecutorNodeCreateRequest",
|
|
122
|
+
"ExecutorNodeCredentialIssue",
|
|
123
|
+
"ExecutorNodeRecord",
|
|
124
|
+
"ExecutorNodeUpdateRequest",
|
|
125
|
+
"MessageRequest",
|
|
126
|
+
"MessageResponse",
|
|
127
|
+
"ResolveInteractionRequest",
|
|
128
|
+
"ResolveInteractionRequestResponse",
|
|
129
|
+
"ResolveInteractionRequestSocketAction",
|
|
130
|
+
"SendCommandSocketAction",
|
|
131
|
+
"SendMessageSocketAction",
|
|
132
|
+
"SessionResponse",
|
|
133
|
+
"SessionSnapshot",
|
|
134
|
+
"ToolInvocationSummary",
|
|
135
|
+
]
|
synapse/api/paths.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
API_PREFIX = "/api"
|
|
4
|
+
|
|
5
|
+
# Keep legacy API-like prefixes reserved in the combined service so the SPA
|
|
6
|
+
# never claims old API URLs after the hard cutover to /api.
|
|
7
|
+
LEGACY_API_ROUTE_PREFIXES = {
|
|
8
|
+
"health",
|
|
9
|
+
"sessions",
|
|
10
|
+
"messages",
|
|
11
|
+
"commands",
|
|
12
|
+
"interaction-requests",
|
|
13
|
+
"personas",
|
|
14
|
+
"executors",
|
|
15
|
+
"connectors",
|
|
16
|
+
"openapi.json",
|
|
17
|
+
"docs",
|
|
18
|
+
"redoc",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SERVICE_RESERVED_ROUTE_PREFIXES = {"api", *LEGACY_API_ROUTE_PREFIXES}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def api_path(path: str) -> str:
|
|
25
|
+
normalized = path if path.startswith("/") else f"/{path}"
|
|
26
|
+
return f"{API_PREFIX}{normalized}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP routes for the new Synapse API."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from uuid import uuid4
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
4
|
+
|
|
5
|
+
from synapse.api.models import CommandRequest, CommandResponse
|
|
6
|
+
from synapse.communication.resolver import TaskResolver, describe_candidates
|
|
7
|
+
from synapse.observability.context import bind_diagnostic_context
|
|
8
|
+
from synapse.protocol import TaskCommand
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/sessions/{session_id}/commands", response_model=CommandResponse)
|
|
14
|
+
async def submit_command(
|
|
15
|
+
session_id: str,
|
|
16
|
+
request: CommandRequest,
|
|
17
|
+
http_request: Request,
|
|
18
|
+
) -> CommandResponse:
|
|
19
|
+
container = http_request.app.state.runtime_container
|
|
20
|
+
try:
|
|
21
|
+
session = container.get_session(session_id)
|
|
22
|
+
except KeyError as exc:
|
|
23
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
24
|
+
|
|
25
|
+
tasks = await session.blackboard.list_tasks()
|
|
26
|
+
resolution = TaskResolver().resolve(tasks, task_id=request.task_id, reference=request.reference)
|
|
27
|
+
if resolution.status == "ambiguous":
|
|
28
|
+
raise HTTPException(
|
|
29
|
+
status_code=409,
|
|
30
|
+
detail=(
|
|
31
|
+
"Task reference is ambiguous. Relevant tasks: "
|
|
32
|
+
f"{describe_candidates(resolution.candidates)}."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
task = resolution.task
|
|
36
|
+
if task is None:
|
|
37
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
38
|
+
validation_error = await session.validate_task_command(task, request.command_type)
|
|
39
|
+
if validation_error is not None:
|
|
40
|
+
raise HTTPException(status_code=409, detail=validation_error)
|
|
41
|
+
|
|
42
|
+
command = TaskCommand(
|
|
43
|
+
command_id=f"cmd-{uuid4().hex[:8]}",
|
|
44
|
+
task_id=task.task_id,
|
|
45
|
+
command_type=request.command_type,
|
|
46
|
+
payload=request.payload,
|
|
47
|
+
created_by="api",
|
|
48
|
+
reason=request.reason,
|
|
49
|
+
)
|
|
50
|
+
request_id = f"http-cmd-{uuid4().hex[:8]}"
|
|
51
|
+
session.observability.api.command_accepted(
|
|
52
|
+
conversation_id=session.session_id,
|
|
53
|
+
request_id=request_id,
|
|
54
|
+
task_id=task.task_id,
|
|
55
|
+
command_type=request.command_type.value,
|
|
56
|
+
transport="http",
|
|
57
|
+
)
|
|
58
|
+
with bind_diagnostic_context(
|
|
59
|
+
conversation_id=session.session_id,
|
|
60
|
+
request_id=request_id,
|
|
61
|
+
task_id=task.task_id,
|
|
62
|
+
):
|
|
63
|
+
await session.apply_command(command)
|
|
64
|
+
session.schedule_execution()
|
|
65
|
+
await session.publish_snapshot()
|
|
66
|
+
return CommandResponse(command_id=command.command_id, affected_task_ids=[task.task_id])
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
4
|
+
|
|
5
|
+
from synapse.api.models import ExecutorNodeCreateRequest, ExecutorNodeUpdateRequest
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _require_session(container, session_id: str):
|
|
11
|
+
try:
|
|
12
|
+
return container.get_session(session_id)
|
|
13
|
+
except KeyError as exc:
|
|
14
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/sessions/{session_id}/executor-nodes")
|
|
18
|
+
async def list_executor_nodes(session_id: str, request: Request):
|
|
19
|
+
container = request.app.state.runtime_container
|
|
20
|
+
_require_session(container, session_id)
|
|
21
|
+
return await container.executor_node_manager.list_nodes()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/sessions/{session_id}/executor-nodes", status_code=201)
|
|
25
|
+
async def create_executor_node(
|
|
26
|
+
session_id: str,
|
|
27
|
+
body: ExecutorNodeCreateRequest,
|
|
28
|
+
request: Request,
|
|
29
|
+
):
|
|
30
|
+
container = request.app.state.runtime_container
|
|
31
|
+
_require_session(container, session_id)
|
|
32
|
+
try:
|
|
33
|
+
issue = await container.executor_node_manager.create_node(
|
|
34
|
+
name=body.name,
|
|
35
|
+
enabled_executors=body.enabled_executors,
|
|
36
|
+
)
|
|
37
|
+
except RuntimeError as exc:
|
|
38
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
39
|
+
await container.publish_session_snapshots()
|
|
40
|
+
return issue
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.patch("/sessions/{session_id}/executor-nodes/{node_id}")
|
|
44
|
+
async def update_executor_node(
|
|
45
|
+
session_id: str,
|
|
46
|
+
node_id: str,
|
|
47
|
+
body: ExecutorNodeUpdateRequest,
|
|
48
|
+
request: Request,
|
|
49
|
+
):
|
|
50
|
+
container = request.app.state.runtime_container
|
|
51
|
+
_require_session(container, session_id)
|
|
52
|
+
try:
|
|
53
|
+
record = await container.executor_node_manager.update_node(
|
|
54
|
+
node_id,
|
|
55
|
+
name=body.name if "name" in body.model_fields_set else None,
|
|
56
|
+
enabled_executors=body.enabled_executors if "enabled_executors" in body.model_fields_set else None,
|
|
57
|
+
)
|
|
58
|
+
except RuntimeError as exc:
|
|
59
|
+
detail = str(exc)
|
|
60
|
+
status_code = 404 if "not found" in detail.lower() else 400
|
|
61
|
+
raise HTTPException(status_code=status_code, detail=detail) from exc
|
|
62
|
+
await container.publish_session_snapshots()
|
|
63
|
+
return record
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.post("/sessions/{session_id}/executor-nodes/{node_id}/credentials/rotate")
|
|
67
|
+
async def rotate_executor_node_credentials(
|
|
68
|
+
session_id: str,
|
|
69
|
+
node_id: str,
|
|
70
|
+
request: Request,
|
|
71
|
+
):
|
|
72
|
+
container = request.app.state.runtime_container
|
|
73
|
+
_require_session(container, session_id)
|
|
74
|
+
try:
|
|
75
|
+
issue = await container.executor_node_manager.rotate_node_credentials(node_id)
|
|
76
|
+
except RuntimeError as exc:
|
|
77
|
+
detail = str(exc)
|
|
78
|
+
status_code = 404 if "not found" in detail.lower() else 400
|
|
79
|
+
raise HTTPException(status_code=status_code, detail=detail) from exc
|
|
80
|
+
await container.publish_session_snapshots()
|
|
81
|
+
return issue
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.post("/sessions/{session_id}/executor-nodes/{node_id}/connect-command")
|
|
85
|
+
async def reveal_executor_node_connect_command(
|
|
86
|
+
session_id: str,
|
|
87
|
+
node_id: str,
|
|
88
|
+
request: Request,
|
|
89
|
+
):
|
|
90
|
+
container = request.app.state.runtime_container
|
|
91
|
+
_require_session(container, session_id)
|
|
92
|
+
try:
|
|
93
|
+
issue = await container.executor_node_manager.reveal_node_credentials(node_id)
|
|
94
|
+
except RuntimeError as exc:
|
|
95
|
+
detail = str(exc)
|
|
96
|
+
lowered = detail.lower()
|
|
97
|
+
if "not found" in lowered:
|
|
98
|
+
status_code = 404
|
|
99
|
+
elif "rotate credentials first" in lowered or "legacy non-retrievable" in lowered:
|
|
100
|
+
status_code = 409
|
|
101
|
+
else:
|
|
102
|
+
status_code = 400
|
|
103
|
+
raise HTTPException(status_code=status_code, detail=detail) from exc
|
|
104
|
+
return issue
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.delete("/sessions/{session_id}/executor-nodes/{node_id}")
|
|
108
|
+
async def delete_executor_node(
|
|
109
|
+
session_id: str,
|
|
110
|
+
node_id: str,
|
|
111
|
+
request: Request,
|
|
112
|
+
):
|
|
113
|
+
container = request.app.state.runtime_container
|
|
114
|
+
_require_session(container, session_id)
|
|
115
|
+
bound_personas = await container.bound_persona_names_for_node(node_id)
|
|
116
|
+
if bound_personas:
|
|
117
|
+
raise HTTPException(
|
|
118
|
+
status_code=409,
|
|
119
|
+
detail=f"Executor node '{node_id}' is still bound to bros: {', '.join(bound_personas)}.",
|
|
120
|
+
)
|
|
121
|
+
deleted = await container.executor_node_manager.delete_node(node_id)
|
|
122
|
+
if not deleted:
|
|
123
|
+
raise HTTPException(status_code=404, detail=f"Executor node '{node_id}' not found.")
|
|
124
|
+
await container.publish_session_snapshots()
|
|
125
|
+
return {"deleted": node_id}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
6
|
+
|
|
7
|
+
from synapse.api.models import ResolveInteractionRequest, ResolveInteractionRequestResponse
|
|
8
|
+
from synapse.observability.context import bind_diagnostic_context
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.post(
|
|
15
|
+
"/sessions/{session_id}/interaction-requests/{request_id}/resolve",
|
|
16
|
+
response_model=ResolveInteractionRequestResponse,
|
|
17
|
+
)
|
|
18
|
+
async def resolve_interaction_request(
|
|
19
|
+
session_id: str,
|
|
20
|
+
request_id: str,
|
|
21
|
+
request: ResolveInteractionRequest,
|
|
22
|
+
http_request: Request,
|
|
23
|
+
) -> ResolveInteractionRequestResponse:
|
|
24
|
+
container = http_request.app.state.runtime_container
|
|
25
|
+
try:
|
|
26
|
+
session = container.get_session(session_id)
|
|
27
|
+
except KeyError as exc:
|
|
28
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
29
|
+
|
|
30
|
+
with bind_diagnostic_context(
|
|
31
|
+
conversation_id=session.session_id,
|
|
32
|
+
request_id=f"http-ireq-{request_id}",
|
|
33
|
+
):
|
|
34
|
+
try:
|
|
35
|
+
affected_task_ids = await session.resolve_interaction_request(
|
|
36
|
+
request_id,
|
|
37
|
+
action=request.action,
|
|
38
|
+
answer_text=request.answer_text,
|
|
39
|
+
option_id=request.option_id,
|
|
40
|
+
reason=request.reason,
|
|
41
|
+
)
|
|
42
|
+
except KeyError as exc:
|
|
43
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
44
|
+
except ValueError as exc:
|
|
45
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
46
|
+
try:
|
|
47
|
+
session.schedule_execution()
|
|
48
|
+
await session.publish_snapshot()
|
|
49
|
+
except Exception:
|
|
50
|
+
LOGGER.warning(
|
|
51
|
+
"Resolved interaction request %s in session %s, but follow-up scheduling failed.",
|
|
52
|
+
request_id,
|
|
53
|
+
session_id,
|
|
54
|
+
exc_info=True,
|
|
55
|
+
)
|
|
56
|
+
return ResolveInteractionRequestResponse(
|
|
57
|
+
request_id=request_id,
|
|
58
|
+
affected_task_ids=affected_task_ids,
|
|
59
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from uuid import uuid4
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
4
|
+
|
|
5
|
+
from synapse.api.models import MessageRequest, MessageResponse, ToolInvocationSummary
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@router.post("/sessions/{session_id}/messages", response_model=MessageResponse)
|
|
11
|
+
async def submit_message(
|
|
12
|
+
session_id: str,
|
|
13
|
+
request: MessageRequest,
|
|
14
|
+
http_request: Request,
|
|
15
|
+
) -> MessageResponse:
|
|
16
|
+
container = http_request.app.state.runtime_container
|
|
17
|
+
try:
|
|
18
|
+
session = container.get_session(session_id)
|
|
19
|
+
except KeyError as exc:
|
|
20
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
21
|
+
|
|
22
|
+
request_id = f"http-msg-{uuid4().hex[:8]}"
|
|
23
|
+
_, completion = await session.submit_message(
|
|
24
|
+
request_id,
|
|
25
|
+
request.text,
|
|
26
|
+
source=request.source,
|
|
27
|
+
start_processing=False,
|
|
28
|
+
)
|
|
29
|
+
session.observability.api.message_accepted(
|
|
30
|
+
conversation_id=session.session_id,
|
|
31
|
+
request_id=request_id,
|
|
32
|
+
transport="http",
|
|
33
|
+
)
|
|
34
|
+
await session.publish_snapshot()
|
|
35
|
+
session.start_message_processing()
|
|
36
|
+
result = await completion
|
|
37
|
+
return MessageResponse(
|
|
38
|
+
message_id=result.message_id,
|
|
39
|
+
reply_text=result.reply_text,
|
|
40
|
+
conversational_act=result.conversational_act,
|
|
41
|
+
affected_task_ids=result.affected_task_ids,
|
|
42
|
+
tool_invocations=[
|
|
43
|
+
ToolInvocationSummary(tool_name=item.tool_name, args=item.args)
|
|
44
|
+
for item in result.tool_invocations
|
|
45
|
+
],
|
|
46
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
6
|
+
|
|
7
|
+
from synapse.api.models import PersonaCreateRequest, PersonaUpdateRequest
|
|
8
|
+
from synapse.communication.persona_pool import load_personas_from_file, save_personas_to_file
|
|
9
|
+
from synapse.protocol import Persona
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("/sessions/{session_id}/personas")
|
|
15
|
+
async def list_personas(session_id: str, request: Request):
|
|
16
|
+
container = request.app.state.runtime_container
|
|
17
|
+
try:
|
|
18
|
+
container.get_session(session_id)
|
|
19
|
+
except KeyError as exc:
|
|
20
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
21
|
+
return load_personas_from_file()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/sessions/{session_id}/personas", status_code=201)
|
|
25
|
+
async def create_persona(
|
|
26
|
+
session_id: str,
|
|
27
|
+
body: PersonaCreateRequest,
|
|
28
|
+
request: Request,
|
|
29
|
+
):
|
|
30
|
+
container = request.app.state.runtime_container
|
|
31
|
+
try:
|
|
32
|
+
container.get_session(session_id)
|
|
33
|
+
except KeyError as exc:
|
|
34
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
35
|
+
if not body.name.strip():
|
|
36
|
+
raise HTTPException(status_code=400, detail="Persona name is required.")
|
|
37
|
+
if body.executor_node_id is not None and not await container.executor_node_manager.node_exists(body.executor_node_id):
|
|
38
|
+
raise HTTPException(status_code=400, detail=f"Executor node '{body.executor_node_id}' not found.")
|
|
39
|
+
normalized_name = body.name.strip()
|
|
40
|
+
persona_id = _generated_persona_id(normalized_name)
|
|
41
|
+
personas = load_personas_from_file()
|
|
42
|
+
persona = Persona(
|
|
43
|
+
persona_id=persona_id,
|
|
44
|
+
name=normalized_name,
|
|
45
|
+
avatar=body.avatar,
|
|
46
|
+
base_prompt=body.base_prompt,
|
|
47
|
+
executor_node_id=body.executor_node_id,
|
|
48
|
+
)
|
|
49
|
+
updated_personas = [*personas, persona]
|
|
50
|
+
save_personas_to_file(updated_personas)
|
|
51
|
+
await container.sync_persisted_personas(updated_personas)
|
|
52
|
+
return persona
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.patch("/sessions/{session_id}/personas/{persona_id}")
|
|
56
|
+
async def update_persona(
|
|
57
|
+
session_id: str,
|
|
58
|
+
persona_id: str,
|
|
59
|
+
body: PersonaUpdateRequest,
|
|
60
|
+
request: Request,
|
|
61
|
+
):
|
|
62
|
+
container = request.app.state.runtime_container
|
|
63
|
+
try:
|
|
64
|
+
container.get_session(session_id)
|
|
65
|
+
except KeyError as exc:
|
|
66
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
67
|
+
personas = load_personas_from_file()
|
|
68
|
+
persona = next((item for item in personas if item.persona_id == persona_id), None)
|
|
69
|
+
if persona is None:
|
|
70
|
+
raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found.")
|
|
71
|
+
updates: dict[str, object] = {}
|
|
72
|
+
if "name" in body.model_fields_set:
|
|
73
|
+
if body.name is None or not body.name.strip():
|
|
74
|
+
raise HTTPException(status_code=400, detail="Persona name is required.")
|
|
75
|
+
updates["name"] = body.name.strip()
|
|
76
|
+
if "avatar" in body.model_fields_set:
|
|
77
|
+
if body.avatar is None:
|
|
78
|
+
raise HTTPException(status_code=400, detail="Persona avatar is required.")
|
|
79
|
+
updates["avatar"] = body.avatar
|
|
80
|
+
if "base_prompt" in body.model_fields_set:
|
|
81
|
+
if body.base_prompt is None:
|
|
82
|
+
raise HTTPException(status_code=400, detail="Persona base prompt is required.")
|
|
83
|
+
updates["base_prompt"] = body.base_prompt
|
|
84
|
+
if "executor_node_id" in body.model_fields_set:
|
|
85
|
+
if body.executor_node_id is not None and not await container.executor_node_manager.node_exists(body.executor_node_id):
|
|
86
|
+
raise HTTPException(status_code=400, detail=f"Executor node '{body.executor_node_id}' not found.")
|
|
87
|
+
updates["executor_node_id"] = body.executor_node_id
|
|
88
|
+
updated = persona.model_copy(update=updates) if updates else persona
|
|
89
|
+
if updates:
|
|
90
|
+
updated_personas = [
|
|
91
|
+
updated if item.persona_id == persona_id else item
|
|
92
|
+
for item in personas
|
|
93
|
+
]
|
|
94
|
+
save_personas_to_file(updated_personas)
|
|
95
|
+
await container.sync_persisted_personas(updated_personas)
|
|
96
|
+
return updated
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.delete("/sessions/{session_id}/personas/{persona_id}")
|
|
100
|
+
async def delete_persona(
|
|
101
|
+
session_id: str,
|
|
102
|
+
persona_id: str,
|
|
103
|
+
request: Request,
|
|
104
|
+
):
|
|
105
|
+
container = request.app.state.runtime_container
|
|
106
|
+
try:
|
|
107
|
+
container.get_session(session_id)
|
|
108
|
+
except KeyError as exc:
|
|
109
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
110
|
+
personas = load_personas_from_file()
|
|
111
|
+
persona = next((item for item in personas if item.persona_id == persona_id), None)
|
|
112
|
+
if persona is None:
|
|
113
|
+
raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found.")
|
|
114
|
+
if await container.persona_is_busy(persona_id):
|
|
115
|
+
raise HTTPException(status_code=409, detail=f"Persona '{persona_id}' is busy and cannot be deleted.")
|
|
116
|
+
updated_personas = [item for item in personas if item.persona_id != persona_id]
|
|
117
|
+
save_personas_to_file(updated_personas)
|
|
118
|
+
await container.sync_persisted_personas(updated_personas)
|
|
119
|
+
return {"deleted": persona_id}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _generated_persona_id(name: str) -> str:
|
|
123
|
+
slug = "-".join(name.strip().lower().split())
|
|
124
|
+
return f"persona-{slug or 'bro'}-{uuid4().hex[:8]}"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from synapse.communication.persona_pool import (
|
|
7
|
+
load_communication_persona_prompt_from_file,
|
|
8
|
+
save_communication_persona_prompt_to_file,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
ALLOWED_CONFIG_KEYS = frozenset({
|
|
14
|
+
"communication_persona_prompt",
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SessionConfigValue(BaseModel):
|
|
19
|
+
value: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_key(key: str) -> None:
|
|
23
|
+
if key not in ALLOWED_CONFIG_KEYS:
|
|
24
|
+
raise HTTPException(
|
|
25
|
+
status_code=400,
|
|
26
|
+
detail=f"Unknown config key '{key}'. Allowed keys: {', '.join(sorted(ALLOWED_CONFIG_KEYS))}",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("/sessions/{session_id}/config/{key}")
|
|
31
|
+
async def get_session_config(session_id: str, key: str, request: Request):
|
|
32
|
+
_validate_key(key)
|
|
33
|
+
container = request.app.state.runtime_container
|
|
34
|
+
try:
|
|
35
|
+
container.get_session(session_id)
|
|
36
|
+
except KeyError as exc:
|
|
37
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
38
|
+
# Read from the persisted file, not the live blackboard.
|
|
39
|
+
# The blackboard value is frozen at session start.
|
|
40
|
+
if key == "communication_persona_prompt":
|
|
41
|
+
value = load_communication_persona_prompt_from_file()
|
|
42
|
+
else:
|
|
43
|
+
value = None
|
|
44
|
+
return {"key": key, "value": value}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.put("/sessions/{session_id}/config/{key}")
|
|
48
|
+
async def put_session_config(
|
|
49
|
+
session_id: str,
|
|
50
|
+
key: str,
|
|
51
|
+
body: SessionConfigValue,
|
|
52
|
+
request: Request,
|
|
53
|
+
):
|
|
54
|
+
_validate_key(key)
|
|
55
|
+
container = request.app.state.runtime_container
|
|
56
|
+
try:
|
|
57
|
+
container.get_session(session_id)
|
|
58
|
+
except KeyError as exc:
|
|
59
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
60
|
+
# Persist to file only. The current session's blackboard is not updated;
|
|
61
|
+
# the new value takes effect on the next session start.
|
|
62
|
+
if key == "communication_persona_prompt":
|
|
63
|
+
save_communication_persona_prompt_to_file(body.value)
|
|
64
|
+
return {"key": key, "value": body.value}
|