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.
Files changed (186) hide show
  1. newbro_cli-0.1.0.dist-info/METADATA +333 -0
  2. newbro_cli-0.1.0.dist-info/RECORD +186 -0
  3. newbro_cli-0.1.0.dist-info/WHEEL +5 -0
  4. newbro_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. newbro_cli-0.1.0.dist-info/top_level.txt +1 -0
  6. synapse/__init__.py +3 -0
  7. synapse/__main__.py +9 -0
  8. synapse/api/__init__.py +1 -0
  9. synapse/api/app.py +54 -0
  10. synapse/api/logging.py +42 -0
  11. synapse/api/models.py +135 -0
  12. synapse/api/paths.py +26 -0
  13. synapse/api/routes/__init__.py +1 -0
  14. synapse/api/routes/commands.py +66 -0
  15. synapse/api/routes/executor_nodes.py +125 -0
  16. synapse/api/routes/health.py +8 -0
  17. synapse/api/routes/interaction_requests.py +59 -0
  18. synapse/api/routes/messages.py +46 -0
  19. synapse/api/routes/personas.py +124 -0
  20. synapse/api/routes/session_config.py +64 -0
  21. synapse/api/routes/sessions.py +129 -0
  22. synapse/api/ws/__init__.py +1 -0
  23. synapse/api/ws/executors.py +69 -0
  24. synapse/api/ws/stream.py +363 -0
  25. synapse/blackboard/__init__.py +14 -0
  26. synapse/blackboard/backends/__init__.py +5 -0
  27. synapse/blackboard/backends/memory.py +491 -0
  28. synapse/blackboard/interfaces.py +167 -0
  29. synapse/blackboard/queries.py +31 -0
  30. synapse/blackboard/revisions.py +30 -0
  31. synapse/blackboard/store.py +31 -0
  32. synapse/blackboard/subscriptions.py +23 -0
  33. synapse/cli/__init__.py +5 -0
  34. synapse/cli/main.py +1958 -0
  35. synapse/communication/__init__.py +6 -0
  36. synapse/communication/brain.py +1274 -0
  37. synapse/communication/context.py +219 -0
  38. synapse/communication/history.py +80 -0
  39. synapse/communication/model.py +107 -0
  40. synapse/communication/models/__init__.py +4 -0
  41. synapse/communication/models/openai.py +331 -0
  42. synapse/communication/models/scripted.py +167 -0
  43. synapse/communication/persona_pool.py +200 -0
  44. synapse/communication/policies/__init__.py +4 -0
  45. synapse/communication/policies/reply_style.py +59 -0
  46. synapse/communication/policies/tool_usage_policy.py +10 -0
  47. synapse/communication/prompts/__init__.py +3 -0
  48. synapse/communication/prompts/base/__init__.py +1 -0
  49. synapse/communication/prompts/base/guardrails.py +6 -0
  50. synapse/communication/prompts/base/identity.py +7 -0
  51. synapse/communication/prompts/base/persona_identity.py +21 -0
  52. synapse/communication/prompts/base/reply_style.py +6 -0
  53. synapse/communication/prompts/base/tool_policy.py +57 -0
  54. synapse/communication/prompts/builders.py +168 -0
  55. synapse/communication/prompts/examples/__init__.py +1 -0
  56. synapse/communication/prompts/examples/notification_style.py +11 -0
  57. synapse/communication/prompts/examples/tool_usage.py +60 -0
  58. synapse/communication/prompts/runtime_context.py +199 -0
  59. synapse/communication/prompts/tasks/__init__.py +1 -0
  60. synapse/communication/prompts/tasks/normal_reply.py +40 -0
  61. synapse/communication/prompts/tasks/proactive_notification.py +14 -0
  62. synapse/communication/resolver.py +156 -0
  63. synapse/communication/tools/__init__.py +256 -0
  64. synapse/communication/tools/add_constraint.py +63 -0
  65. synapse/communication/tools/add_task_note.py +59 -0
  66. synapse/communication/tools/base.py +22 -0
  67. synapse/communication/tools/control_task.py +82 -0
  68. synapse/communication/tools/create_task.py +127 -0
  69. synapse/communication/tools/list_tasks.py +53 -0
  70. synapse/communication/tools/query_task_detail.py +57 -0
  71. synapse/communication/tools/query_task_summary.py +34 -0
  72. synapse/communication/tools/resolve_interaction_request.py +67 -0
  73. synapse/communication/tools/update_task.py +109 -0
  74. synapse/communication/types.py +22 -0
  75. synapse/config_home.py +72 -0
  76. synapse/connectors/__init__.py +1 -0
  77. synapse/connectors/base/__init__.py +20 -0
  78. synapse/connectors/base/bindings.py +145 -0
  79. synapse/connectors/base/module.py +48 -0
  80. synapse/connectors/base/transport.py +202 -0
  81. synapse/connectors/host/__init__.py +17 -0
  82. synapse/connectors/host/app.py +60 -0
  83. synapse/connectors/host/catalog.py +29 -0
  84. synapse/connectors/host/config.py +164 -0
  85. synapse/connectors/host/registry.py +21 -0
  86. synapse/connectors/voice/__init__.py +1 -0
  87. synapse/connectors/voice/agora_convoai/__init__.py +20 -0
  88. synapse/connectors/voice/agora_convoai/app.py +4 -0
  89. synapse/connectors/voice/agora_convoai/models.py +116 -0
  90. synapse/connectors/voice/agora_convoai/module.py +391 -0
  91. synapse/connectors/voice/agora_convoai/service.py +513 -0
  92. synapse/connectors/voice/agora_convoai/session_service.py +286 -0
  93. synapse/connectors/voice/agora_convoai/settings.py +223 -0
  94. synapse/connectors/voice/agora_convoai/token_utils.py +153 -0
  95. synapse/envfile.py +25 -0
  96. synapse/execution/__init__.py +21 -0
  97. synapse/execution/assignment.py +53 -0
  98. synapse/execution/brain.py +45 -0
  99. synapse/execution/mode_manager.py +105 -0
  100. synapse/execution/reconcile.py +178 -0
  101. synapse/execution/run_manager.py +184 -0
  102. synapse/execution/scheduler.py +13 -0
  103. synapse/execution/session_manager.py +122 -0
  104. synapse/execution/summary_manager.py +84 -0
  105. synapse/executors/__init__.py +1 -0
  106. synapse/executors/adapters/__init__.py +17 -0
  107. synapse/executors/adapters/acpx/__init__.py +4 -0
  108. synapse/executors/adapters/acpx/executor.py +610 -0
  109. synapse/executors/adapters/acpx/session.py +117 -0
  110. synapse/executors/adapters/codex/__init__.py +4 -0
  111. synapse/executors/adapters/codex/client.py +182 -0
  112. synapse/executors/adapters/codex/executor.py +455 -0
  113. synapse/executors/adapters/codex/jsonrpc.py +108 -0
  114. synapse/executors/adapters/codex/session.py +124 -0
  115. synapse/executors/adapters/hosted/__init__.py +3 -0
  116. synapse/executors/adapters/hosted/executor.py +95 -0
  117. synapse/executors/adapters/mock/__init__.py +5 -0
  118. synapse/executors/adapters/mock/config.py +7 -0
  119. synapse/executors/adapters/mock/executor.py +91 -0
  120. synapse/executors/adapters/mock/session.py +7 -0
  121. synapse/executors/core/__init__.py +19 -0
  122. synapse/executors/core/capabilities.py +12 -0
  123. synapse/executors/core/events.py +22 -0
  124. synapse/executors/core/executor.py +32 -0
  125. synapse/executors/core/registry.py +28 -0
  126. synapse/executors/core/results.py +12 -0
  127. synapse/executors/core/session.py +12 -0
  128. synapse/executors/node/__init__.py +15 -0
  129. synapse/executors/node/__main__.py +43 -0
  130. synapse/executors/node/config.py +130 -0
  131. synapse/executors/node/registry.py +310 -0
  132. synapse/executors/node/service.py +397 -0
  133. synapse/infrastructure/__init__.py +1 -0
  134. synapse/infrastructure/llm/__init__.py +3 -0
  135. synapse/infrastructure/llm/openai_provider.py +454 -0
  136. synapse/interaction/__init__.py +3 -0
  137. synapse/interaction/manager.py +353 -0
  138. synapse/interaction/sanitization.py +76 -0
  139. synapse/notification/__init__.py +14 -0
  140. synapse/notification/candidate_builder.py +136 -0
  141. synapse/notification/manager.py +179 -0
  142. synapse/notification/policy.py +91 -0
  143. synapse/observability/__init__.py +25 -0
  144. synapse/observability/bootstrap.py +146 -0
  145. synapse/observability/context.py +47 -0
  146. synapse/observability/emitters/__init__.py +29 -0
  147. synapse/observability/emitters/api.py +87 -0
  148. synapse/observability/emitters/blackboard.py +76 -0
  149. synapse/observability/emitters/communication.py +178 -0
  150. synapse/observability/emitters/execution.py +154 -0
  151. synapse/observability/emitters/notification.py +111 -0
  152. synapse/observability/logger.py +73 -0
  153. synapse/observability/reason_codes.py +14 -0
  154. synapse/observability/redaction.py +117 -0
  155. synapse/observability/schema.py +57 -0
  156. synapse/observability/sinks/__init__.py +4 -0
  157. synapse/observability/sinks/pretty.py +216 -0
  158. synapse/observability/sinks/stdout.py +16 -0
  159. synapse/observability/sinks/types.py +10 -0
  160. synapse/observability/store.py +85 -0
  161. synapse/protocol/__init__.py +97 -0
  162. synapse/protocol/assignment.py +9 -0
  163. synapse/protocol/command.py +14 -0
  164. synapse/protocol/enums.py +138 -0
  165. synapse/protocol/execution_mode.py +12 -0
  166. synapse/protocol/executor_node.py +120 -0
  167. synapse/protocol/interaction.py +47 -0
  168. synapse/protocol/interruption.py +13 -0
  169. synapse/protocol/mutation.py +16 -0
  170. synapse/protocol/notification.py +22 -0
  171. synapse/protocol/persona.py +19 -0
  172. synapse/protocol/run.py +20 -0
  173. synapse/protocol/session.py +43 -0
  174. synapse/protocol/summary.py +11 -0
  175. synapse/protocol/task.py +22 -0
  176. synapse/protocol/task_execution_detail.py +14 -0
  177. synapse/runtime/__init__.py +28 -0
  178. synapse/runtime/bootstrap.py +29 -0
  179. synapse/runtime/config.py +209 -0
  180. synapse/runtime/container.py +102 -0
  181. synapse/runtime/executor_node_manager.py +477 -0
  182. synapse/runtime/models.py +112 -0
  183. synapse/runtime/session.py +1212 -0
  184. synapse/service/__init__.py +3 -0
  185. synapse/service/app.py +96 -0
  186. 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,8 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+
6
+ @router.get("/health")
7
+ async def health() -> dict[str, str]:
8
+ return {"status": "ok"}
@@ -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}