synapse-orch-ai 1.3.4 → 1.4.2

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 (202) hide show
  1. package/backend/core/api_key_middleware.py +39 -0
  2. package/backend/core/api_keys.py +143 -0
  3. package/backend/core/config.py +39 -0
  4. package/backend/core/internal_auth.py +62 -0
  5. package/backend/core/models.py +3 -0
  6. package/backend/core/orchestration/engine.py +4 -3
  7. package/backend/core/routes/api_keys.py +46 -0
  8. package/backend/core/routes/api_v1.py +539 -0
  9. package/backend/core/routes/auth.py +43 -1
  10. package/backend/core/routes/settings.py +29 -2
  11. package/backend/core/server.py +10 -2
  12. package/backend/core/usage_tracker.py +3 -2
  13. package/backend/core/user_auth.py +48 -0
  14. package/backend/requirements-coding.txt +1 -1
  15. package/backend/tools/code_search.py +46 -24
  16. package/frontend-build/.next/BUILD_ID +1 -1
  17. package/frontend-build/.next/app-path-routes-manifest.json +4 -0
  18. package/frontend-build/.next/build-manifest.json +3 -3
  19. package/frontend-build/.next/prerender-manifest.json +27 -3
  20. package/frontend-build/.next/required-server-files.json +2 -0
  21. package/frontend-build/.next/routes-manifest.json +24 -0
  22. package/frontend-build/.next/server/app/_global-error/page.js +1 -1
  23. package/frontend-build/.next/server/app/_global-error/page.js.nft.json +1 -1
  24. package/frontend-build/.next/server/app/_global-error.html +1 -1
  25. package/frontend-build/.next/server/app/_global-error.rsc +1 -1
  26. package/frontend-build/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  27. package/frontend-build/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  28. package/frontend-build/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  29. package/frontend-build/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  30. package/frontend-build/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  31. package/frontend-build/.next/server/app/_not-found/page.js +1 -1
  32. package/frontend-build/.next/server/app/_not-found/page.js.nft.json +1 -1
  33. package/frontend-build/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  34. package/frontend-build/.next/server/app/_not-found.html +1 -1
  35. package/frontend-build/.next/server/app/_not-found.rsc +2 -2
  36. package/frontend-build/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  37. package/frontend-build/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  38. package/frontend-build/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  39. package/frontend-build/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  40. package/frontend-build/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  41. package/frontend-build/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  42. package/frontend-build/.next/server/app/api/agent-types/route.js +1 -1
  43. package/frontend-build/.next/server/app/api/agent-types/route.js.nft.json +1 -1
  44. package/frontend-build/.next/server/app/api/agents/generate-prompt/route.js +1 -1
  45. package/frontend-build/.next/server/app/api/agents/generate-prompt/route.js.nft.json +1 -1
  46. package/frontend-build/.next/server/app/api/auth/login/route/app-paths-manifest.json +3 -0
  47. package/frontend-build/.next/server/app/api/auth/login/route/build-manifest.json +9 -0
  48. package/frontend-build/.next/server/app/api/auth/login/route/server-reference-manifest.json +4 -0
  49. package/frontend-build/.next/server/app/api/auth/login/route.js +6 -0
  50. package/frontend-build/.next/server/app/api/auth/login/route.js.map +5 -0
  51. package/frontend-build/.next/server/app/api/auth/login/route.js.nft.json +1 -0
  52. package/frontend-build/.next/server/app/api/auth/login/route_client-reference-manifest.js +3 -0
  53. package/frontend-build/.next/server/app/api/auth/logout/route/app-paths-manifest.json +3 -0
  54. package/frontend-build/.next/server/app/api/auth/logout/route/build-manifest.json +9 -0
  55. package/frontend-build/.next/server/app/api/auth/logout/route/server-reference-manifest.json +4 -0
  56. package/frontend-build/.next/server/app/api/auth/logout/route.js +6 -0
  57. package/frontend-build/.next/server/app/api/auth/logout/route.js.map +5 -0
  58. package/frontend-build/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
  59. package/frontend-build/.next/server/app/api/auth/logout/route_client-reference-manifest.js +3 -0
  60. package/frontend-build/.next/server/app/api/auth/status/route/app-paths-manifest.json +3 -0
  61. package/frontend-build/.next/server/app/api/auth/status/route/build-manifest.json +9 -0
  62. package/frontend-build/.next/server/app/api/auth/status/route/server-reference-manifest.json +4 -0
  63. package/frontend-build/.next/server/app/api/auth/status/route.js +6 -0
  64. package/frontend-build/.next/server/app/api/auth/status/route.js.map +5 -0
  65. package/frontend-build/.next/server/app/api/auth/status/route.js.nft.json +1 -0
  66. package/frontend-build/.next/server/app/api/auth/status/route_client-reference-manifest.js +3 -0
  67. package/frontend-build/.next/server/app/api/builder/chat/route.js +1 -1
  68. package/frontend-build/.next/server/app/api/builder/chat/route.js.nft.json +1 -1
  69. package/frontend-build/.next/server/app/api/builder/resume/route.js +1 -1
  70. package/frontend-build/.next/server/app/api/builder/resume/route.js.nft.json +1 -1
  71. package/frontend-build/.next/server/app/api/chat/route.js +1 -1
  72. package/frontend-build/.next/server/app/api/chat/route.js.nft.json +1 -1
  73. package/frontend-build/.next/server/app/api/chat/stream/route.js +1 -1
  74. package/frontend-build/.next/server/app/api/chat/stream/route.js.nft.json +1 -1
  75. package/frontend-build/.next/server/app/api/logs/[type]/[run_id]/route.js +1 -1
  76. package/frontend-build/.next/server/app/api/logs/[type]/[run_id]/route.js.nft.json +1 -1
  77. package/frontend-build/.next/server/app/api/logs/[type]/route.js +1 -1
  78. package/frontend-build/.next/server/app/api/logs/[type]/route.js.nft.json +1 -1
  79. package/frontend-build/.next/server/app/api/models/route.js +1 -1
  80. package/frontend-build/.next/server/app/api/models/route.js.nft.json +1 -1
  81. package/frontend-build/.next/server/app/api/orchestrations/[orch_id]/run/route.js +1 -1
  82. package/frontend-build/.next/server/app/api/orchestrations/[orch_id]/run/route.js.nft.json +1 -1
  83. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/human-input/route.js +1 -1
  84. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/human-input/route.js.nft.json +1 -1
  85. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/resume/route.js +1 -1
  86. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/resume/route.js.nft.json +1 -1
  87. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/route.js +1 -1
  88. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/route.js.nft.json +1 -1
  89. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/run/route.js +1 -1
  90. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/run/route.js.nft.json +1 -1
  91. package/frontend-build/.next/server/app/api/schedules/route.js +1 -1
  92. package/frontend-build/.next/server/app/api/schedules/route.js.nft.json +1 -1
  93. package/frontend-build/.next/server/app/index.html +1 -1
  94. package/frontend-build/.next/server/app/index.rsc +3 -3
  95. package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  96. package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +3 -3
  97. package/frontend-build/.next/server/app/index.segments/_head.segment.rsc +1 -1
  98. package/frontend-build/.next/server/app/index.segments/_index.segment.rsc +2 -2
  99. package/frontend-build/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  100. package/frontend-build/.next/server/app/login/page/app-paths-manifest.json +3 -0
  101. package/frontend-build/.next/server/app/login/page/build-manifest.json +17 -0
  102. package/frontend-build/.next/server/app/login/page/next-font-manifest.json +14 -0
  103. package/frontend-build/.next/server/app/login/page/react-loadable-manifest.json +1 -0
  104. package/frontend-build/.next/server/app/login/page/server-reference-manifest.json +4 -0
  105. package/frontend-build/.next/server/app/login/page.js +14 -0
  106. package/frontend-build/.next/server/app/login/page.js.map +5 -0
  107. package/frontend-build/.next/server/app/login/page.js.nft.json +1 -0
  108. package/frontend-build/.next/server/app/login/page_client-reference-manifest.js +3 -0
  109. package/frontend-build/.next/server/app/login.html +1 -0
  110. package/frontend-build/.next/server/app/login.meta +15 -0
  111. package/frontend-build/.next/server/app/login.rsc +27 -0
  112. package/frontend-build/.next/server/app/login.segments/_full.segment.rsc +27 -0
  113. package/frontend-build/.next/server/app/login.segments/_head.segment.rsc +6 -0
  114. package/frontend-build/.next/server/app/login.segments/_index.segment.rsc +7 -0
  115. package/frontend-build/.next/server/app/login.segments/_tree.segment.rsc +8 -0
  116. package/frontend-build/.next/server/app/login.segments/login/__PAGE__.segment.rsc +9 -0
  117. package/frontend-build/.next/server/app/login.segments/login.segment.rsc +5 -0
  118. package/frontend-build/.next/server/app/page.js +1 -1
  119. package/frontend-build/.next/server/app/page.js.nft.json +1 -1
  120. package/frontend-build/.next/server/app/page_client-reference-manifest.js +1 -1
  121. package/frontend-build/.next/server/app/settings/[tab]/page.js +1 -1
  122. package/frontend-build/.next/server/app/settings/[tab]/page.js.nft.json +1 -1
  123. package/frontend-build/.next/server/app/settings/[tab]/page_client-reference-manifest.js +1 -1
  124. package/frontend-build/.next/server/app-paths-manifest.json +4 -0
  125. package/frontend-build/.next/server/chunks/[root-of-the-server]__00ci3m0._.js +3 -0
  126. package/frontend-build/.next/server/chunks/[root-of-the-server]__01~.b7w._.js +3 -0
  127. package/frontend-build/.next/server/chunks/[root-of-the-server]__066njqe._.js +3 -0
  128. package/frontend-build/.next/server/chunks/[root-of-the-server]__08cioyi._.js +3 -0
  129. package/frontend-build/.next/server/chunks/[root-of-the-server]__0df0b6v._.js +3 -0
  130. package/frontend-build/.next/server/chunks/[root-of-the-server]__0efx8a5._.js +3 -0
  131. package/frontend-build/.next/server/chunks/[root-of-the-server]__0j8-xkl._.js +1 -1
  132. package/frontend-build/.next/server/chunks/[root-of-the-server]__0kk2o8d._.js +3 -0
  133. package/frontend-build/.next/server/chunks/[root-of-the-server]__0kyyn1g._.js +3 -0
  134. package/frontend-build/.next/server/chunks/[root-of-the-server]__0o2ckji._.js +3 -0
  135. package/frontend-build/.next/server/chunks/[root-of-the-server]__0r1_rdp._.js +3 -0
  136. package/frontend-build/.next/server/chunks/[root-of-the-server]__0s65g6m._.js +3 -0
  137. package/frontend-build/.next/server/chunks/[root-of-the-server]__0u8_aw2._.js +3 -0
  138. package/frontend-build/.next/server/chunks/[root-of-the-server]__0z5q5.1._.js +3 -0
  139. package/frontend-build/.next/server/chunks/[root-of-the-server]__0~o635_._.js +3 -0
  140. package/frontend-build/.next/server/chunks/[root-of-the-server]__10p49e~._.js +3 -0
  141. package/frontend-build/.next/server/chunks/[root-of-the-server]__115~uq6._.js +3 -0
  142. package/frontend-build/.next/server/chunks/[root-of-the-server]__12ug7cf._.js +3 -0
  143. package/frontend-build/.next/server/chunks/[root-of-the-server]__12vs3wg._.js +3 -0
  144. package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_login_route_actions_0zukc38.js +3 -0
  145. package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_logout_route_actions_0fm~3ij.js +3 -0
  146. package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_status_route_actions_0rmqb6l.js +3 -0
  147. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__08l1kmh._.js +3 -0
  148. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__09c9368._.js +3 -0
  149. package/frontend-build/.next/server/chunks/ssr/_0b~n.nn._.js +91 -19
  150. package/frontend-build/.next/server/chunks/ssr/_0rwng5.._.js +4 -0
  151. package/frontend-build/.next/server/chunks/ssr/_next-internal_server_app_login_page_actions_02kefem.js +3 -0
  152. package/frontend-build/.next/server/chunks/ssr/node_modules_0g2b~5_._.js +6 -0
  153. package/frontend-build/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_0__0o-2._.js +3 -0
  154. package/frontend-build/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0328vov.js +4 -0
  155. package/frontend-build/.next/server/chunks/ssr/src_app_login_page_tsx_0jt.sij._.js +3 -0
  156. package/frontend-build/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js +11 -0
  157. package/frontend-build/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js +3 -0
  158. package/frontend-build/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js +3 -0
  159. package/frontend-build/.next/server/middleware-build-manifest.js +3 -3
  160. package/frontend-build/.next/server/middleware-manifest.json +30 -2
  161. package/frontend-build/.next/server/next-font-manifest.js +1 -1
  162. package/frontend-build/.next/server/next-font-manifest.json +7 -0
  163. package/frontend-build/.next/server/pages/404.html +1 -1
  164. package/frontend-build/.next/server/pages/500.html +1 -1
  165. package/frontend-build/.next/server/server-reference-manifest.js +1 -1
  166. package/frontend-build/.next/server/server-reference-manifest.json +1 -1
  167. package/frontend-build/.next/static/Yru6SbqHLpZ5TlStu7jpG/_clientMiddlewareManifest.js +6 -0
  168. package/frontend-build/.next/static/chunks/0e0doloxq75td.js +1 -0
  169. package/frontend-build/.next/static/chunks/0k0-nxx35rq9v.js +1 -0
  170. package/frontend-build/.next/static/chunks/0w5f~4w31j03v.js +2 -0
  171. package/frontend-build/.next/static/chunks/0yq31mtv7c.bs.css +1 -0
  172. package/frontend-build/.next/static/chunks/18b0o~cnvc.gn.js +124 -0
  173. package/frontend-build/package.json +1 -0
  174. package/frontend-build/server.js +1 -1
  175. package/package.json +1 -1
  176. package/frontend-build/.next/server/chunks/[root-of-the-server]__00sljji._.js +0 -3
  177. package/frontend-build/.next/server/chunks/[root-of-the-server]__03e7r3a._.js +0 -3
  178. package/frontend-build/.next/server/chunks/[root-of-the-server]__053~3b.._.js +0 -3
  179. package/frontend-build/.next/server/chunks/[root-of-the-server]__0afb7iz._.js +0 -3
  180. package/frontend-build/.next/server/chunks/[root-of-the-server]__0bc5iuk._.js +0 -3
  181. package/frontend-build/.next/server/chunks/[root-of-the-server]__0bygctj._.js +0 -3
  182. package/frontend-build/.next/server/chunks/[root-of-the-server]__0c7o1w_._.js +0 -3
  183. package/frontend-build/.next/server/chunks/[root-of-the-server]__0cy6bl4._.js +0 -3
  184. package/frontend-build/.next/server/chunks/[root-of-the-server]__0dmb8p1._.js +0 -3
  185. package/frontend-build/.next/server/chunks/[root-of-the-server]__0gx4j6x._.js +0 -3
  186. package/frontend-build/.next/server/chunks/[root-of-the-server]__0h2-vsq._.js +0 -3
  187. package/frontend-build/.next/server/chunks/[root-of-the-server]__0kkdqe3._.js +0 -3
  188. package/frontend-build/.next/server/chunks/[root-of-the-server]__0n1p1jk._.js +0 -3
  189. package/frontend-build/.next/server/chunks/[root-of-the-server]__0xccig4._.js +0 -3
  190. package/frontend-build/.next/server/chunks/[root-of-the-server]__13dxl3m._.js +0 -3
  191. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__0ssmzpx._.js +0 -3
  192. package/frontend-build/.next/server/chunks/ssr/_0ayz11y._.js +0 -4
  193. package/frontend-build/.next/server/chunks/ssr/node_modules_0zhhlrq._.js +0 -6
  194. package/frontend-build/.next/server/chunks/ssr/node_modules_1041ur2._.js +0 -6
  195. package/frontend-build/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_0ag5..o._.js +0 -3
  196. package/frontend-build/.next/static/B6NiRZeoBRJSm9jT1wyru/_clientMiddlewareManifest.js +0 -1
  197. package/frontend-build/.next/static/chunks/0n53o._htd6sy.js +0 -2
  198. package/frontend-build/.next/static/chunks/0n_aip.6onjkv.js +0 -52
  199. package/frontend-build/.next/static/chunks/0or4d2ll~v9-c.css +0 -1
  200. package/frontend-build/.next/static/chunks/0vn.s1wic2p_8.js +0 -1
  201. /package/frontend-build/.next/static/{B6NiRZeoBRJSm9jT1wyru → Yru6SbqHLpZ5TlStu7jpG}/_buildManifest.js +0 -0
  202. /package/frontend-build/.next/static/{B6NiRZeoBRJSm9jT1wyru → Yru6SbqHLpZ5TlStu7jpG}/_ssgManifest.js +0 -0
@@ -0,0 +1,539 @@
1
+ """
2
+ V1 External API Endpoints
3
+ --------------------------
4
+ Programmatic API for external apps to interact with Synapse agents
5
+ and orchestrations. All routes are protected by API key auth (Bearer token).
6
+
7
+ Endpoints:
8
+ POST /chat — Sync chat (returns only final response)
9
+ POST /chat/stream — SSE chat (all events)
10
+ POST /orchestrations/{orch_id}/run — Start orchestration (sync)
11
+ POST /orchestrations/{orch_id}/run/stream — Start orchestration (SSE)
12
+ POST /orchestrations/runs/{run_id}/resume — Resume after human input (sync)
13
+ POST /orchestrations/runs/{run_id}/resume/stream — Resume after human input (SSE)
14
+ """
15
+ import asyncio
16
+ import json
17
+ import logging
18
+ import time
19
+ import uuid
20
+
21
+ from fastapi import APIRouter, Depends, HTTPException
22
+ from fastapi.responses import StreamingResponse
23
+ from pydantic import BaseModel
24
+
25
+ from core.api_key_middleware import require_api_key
26
+
27
+ router = APIRouter()
28
+ log = logging.getLogger("api_v1")
29
+
30
+
31
+ # ── Request / Response Models ────────────────────────────────────────────────
32
+
33
+ class V1ChatRequest(BaseModel):
34
+ message: str
35
+ agent: str | None = None # agent name or ID (optional — first agent if omitted)
36
+ session_id: str | None = None # for conversation continuity (auto-generated if omitted)
37
+ images: list[str] = [] # optional base64 images
38
+
39
+
40
+ class V1OrchestrationRunRequest(BaseModel):
41
+ message: str = ""
42
+
43
+
44
+ class V1ResumeRequest(BaseModel):
45
+ response: dict | str = {} # human input fields
46
+
47
+
48
+ # ── Helpers ──────────────────────────────────────────────────────────────────
49
+
50
+ def _resolve_agent_for_api(agent_identifier: str | None) -> dict | None:
51
+ """Find agent by name (case-insensitive) or ID. Falls back to first agent."""
52
+ from core.routes.agents import load_user_agents
53
+ agents = load_user_agents()
54
+ if not agents:
55
+ return None
56
+ if not agent_identifier:
57
+ return agents[0]
58
+ # Exact ID match
59
+ by_id = next((a for a in agents if a["id"] == agent_identifier), None)
60
+ if by_id:
61
+ return by_id
62
+ # Case-insensitive name match
63
+ lower = agent_identifier.lower()
64
+ by_name = next((a for a in agents if a["name"].lower() == lower), None)
65
+ if by_name:
66
+ return by_name
67
+ # Partial name match
68
+ partial = next((a for a in agents if lower in a["name"].lower()), None)
69
+ if partial:
70
+ return partial
71
+ # Fallback to first agent
72
+ return agents[0]
73
+
74
+
75
+ def _build_chat_request(body: V1ChatRequest, agent: dict) -> "ChatRequest":
76
+ """Convert V1ChatRequest into the internal ChatRequest model."""
77
+ from core.models import ChatRequest
78
+ session_id = body.session_id or f"api_{uuid.uuid4().hex[:12]}"
79
+ return ChatRequest(
80
+ message=body.message,
81
+ session_id=session_id,
82
+ agent_id=agent["id"],
83
+ images=body.images,
84
+ )
85
+
86
+
87
+ def _format_sse_event(event: dict) -> str:
88
+ """Format an event dict as an SSE data line."""
89
+ return f"data: {json.dumps(event, default=str)}\n\n"
90
+
91
+
92
+ # ── Chat Endpoints ──────────────────────────────────────────────────────────
93
+
94
+ @router.post("/chat")
95
+ async def v1_chat(body: V1ChatRequest, key_record: dict = Depends(require_api_key)):
96
+ """Synchronous chat — returns only the final response."""
97
+ agent = _resolve_agent_for_api(body.agent)
98
+ if not agent:
99
+ raise HTTPException(status_code=400, detail="No agents configured")
100
+
101
+ import core.server as _server
102
+ if not _server.agent_sessions:
103
+ raise HTTPException(status_code=503, detail="No agent sessions available. Server may still be starting.")
104
+
105
+ chat_request = _build_chat_request(body, agent)
106
+ from core.react_engine import run_react_loop
107
+
108
+ final_event = None
109
+ error_msg = None
110
+
111
+ try:
112
+ async for event in run_react_loop(chat_request, _server):
113
+ etype = event.get("type", "")
114
+ if etype == "final":
115
+ final_event = event
116
+ elif etype == "error":
117
+ error_msg = event.get("message", "Unknown error")
118
+ except Exception as exc:
119
+ log.exception("[v1/chat] Unhandled error for session=%s", chat_request.session_id)
120
+ raise HTTPException(status_code=500, detail="An internal error occurred. Check server logs for details.")
121
+
122
+ if error_msg:
123
+ log.error("[v1/chat] Agent error for session=%s: %s", chat_request.session_id, error_msg)
124
+ raise HTTPException(status_code=500, detail="The agent encountered an error processing your request.")
125
+
126
+ response_text = "I completed the requested actions."
127
+ if final_event:
128
+ response_text = final_event.get("response", response_text)
129
+
130
+ return {
131
+ "response": response_text,
132
+ "agent_id": agent["id"],
133
+ "agent_name": agent["name"],
134
+ "session_id": chat_request.session_id,
135
+ }
136
+
137
+
138
+ @router.post("/chat/stream")
139
+ async def v1_chat_stream(body: V1ChatRequest, key_record: dict = Depends(require_api_key)):
140
+ """SSE streaming chat — returns all events."""
141
+ agent = _resolve_agent_for_api(body.agent)
142
+ if not agent:
143
+ raise HTTPException(status_code=400, detail="No agents configured")
144
+
145
+ import core.server as _server
146
+ if not _server.agent_sessions:
147
+ raise HTTPException(status_code=503, detail="No agent sessions available. Server may still be starting.")
148
+
149
+ chat_request = _build_chat_request(body, agent)
150
+
151
+ async def event_generator():
152
+ from core.react_engine import run_react_loop
153
+ try:
154
+ # Emit session info first so the consumer can capture the session_id
155
+ yield _format_sse_event({
156
+ "type": "session",
157
+ "session_id": chat_request.session_id,
158
+ "agent_id": agent["id"],
159
+ "agent_name": agent["name"],
160
+ })
161
+ async for event in run_react_loop(chat_request, _server):
162
+ etype = event.get("type", "")
163
+
164
+ if etype == "status":
165
+ yield _format_sse_event({"type": "status", "message": event["message"]})
166
+
167
+ elif etype == "thinking":
168
+ yield _format_sse_event({
169
+ "type": "thinking",
170
+ "message": event.get("message", ""),
171
+ })
172
+
173
+ elif etype == "tool_execution":
174
+ yield _format_sse_event({
175
+ "type": "tool_execution",
176
+ "tool_name": event["tool_name"],
177
+ "args": event["args"],
178
+ })
179
+
180
+ elif etype == "tool_result":
181
+ yield _format_sse_event({
182
+ "type": "tool_result",
183
+ "tool_name": event["tool_name"],
184
+ "preview": event["preview"],
185
+ })
186
+
187
+ elif etype == "llm_thought":
188
+ yield _format_sse_event({
189
+ "type": "llm_thought",
190
+ "thought": event["thought"],
191
+ "turn": event.get("turn", 1),
192
+ })
193
+
194
+ elif etype == "final":
195
+ yield _format_sse_event({
196
+ "type": "response",
197
+ "content": event.get("response", ""),
198
+ "intent": event.get("intent", "chat"),
199
+ "session_id": chat_request.session_id,
200
+ })
201
+ yield _format_sse_event({"type": "done"})
202
+
203
+ elif etype == "error":
204
+ log.error("[v1/chat/stream] Agent error session=%s: %s", chat_request.session_id, event.get("message"))
205
+ yield _format_sse_event({"type": "error", "message": "The agent encountered an error processing your request."})
206
+
207
+ await asyncio.sleep(0)
208
+
209
+ except Exception as e:
210
+ log.exception("[v1/chat/stream] Unhandled error session=%s", chat_request.session_id)
211
+ yield _format_sse_event({"type": "error", "message": "An internal error occurred. Check server logs for details."})
212
+
213
+ return StreamingResponse(
214
+ event_generator(),
215
+ media_type="text/event-stream",
216
+ headers={
217
+ "Cache-Control": "no-cache",
218
+ "Connection": "keep-alive",
219
+ "X-Accel-Buffering": "no",
220
+ },
221
+ )
222
+
223
+
224
+ # ── Orchestration Endpoints ─────────────────────────────────────────────────
225
+
226
+ @router.post("/orchestrations/{orch_id}/run")
227
+ async def v1_orchestration_run(
228
+ orch_id: str,
229
+ body: V1OrchestrationRunRequest,
230
+ key_record: dict = Depends(require_api_key),
231
+ ):
232
+ """Start an orchestration (sync). Returns final result or human_input_required."""
233
+ import core.server as _server
234
+ from core.routes.orchestrations import load_orchestrations
235
+ from core.models_orchestration import Orchestration
236
+ from core.orchestration.engine import OrchestrationEngine
237
+
238
+ orchs = load_orchestrations()
239
+ orch_data = next((o for o in orchs if o["id"] == orch_id), None)
240
+ if not orch_data:
241
+ raise HTTPException(status_code=404, detail=f"Orchestration '{orch_id}' not found")
242
+
243
+ run_id = f"run_{orch_id}_{int(time.time() * 1000)}"
244
+ orch = Orchestration.model_validate(orch_data)
245
+ engine = OrchestrationEngine(orch, _server)
246
+
247
+ final_response = None
248
+ human_input_event = None
249
+ step_history = []
250
+ shared_state = {}
251
+ status = "running"
252
+
253
+ async for event in engine.run(body.message, run_id):
254
+ etype = event.get("type", "")
255
+
256
+ if etype == "human_input_required":
257
+ human_input_event = event
258
+ status = "paused"
259
+ break
260
+
261
+ if etype == "orchestration_complete":
262
+ status = event.get("status", "completed")
263
+ shared_state = event.get("final_state", {})
264
+
265
+ if etype == "final":
266
+ final_response = event.get("response", "")
267
+ step_history = event.get("data", {}).get("step_history", []) if event.get("data") else []
268
+
269
+ if human_input_event:
270
+ return {
271
+ "status": "paused",
272
+ "run_id": run_id,
273
+ "human_input_required": {
274
+ "step_id": human_input_event.get("orch_step_id"),
275
+ "prompt": human_input_event.get("prompt"),
276
+ "fields": human_input_event.get("fields", []),
277
+ "agent_context": human_input_event.get("agent_context"),
278
+ },
279
+ }
280
+
281
+ return {
282
+ "status": status,
283
+ "run_id": run_id,
284
+ "response": final_response or f"Orchestration {status}.",
285
+ "shared_state": shared_state,
286
+ "step_history": step_history,
287
+ }
288
+
289
+
290
+ @router.post("/orchestrations/{orch_id}/run/stream")
291
+ async def v1_orchestration_run_stream(
292
+ orch_id: str,
293
+ body: V1OrchestrationRunRequest,
294
+ key_record: dict = Depends(require_api_key),
295
+ ):
296
+ """Start an orchestration (SSE stream)."""
297
+ import core.server as _server
298
+ from core.routes.orchestrations import load_orchestrations
299
+ from core.models_orchestration import Orchestration
300
+ from core.orchestration.engine import OrchestrationEngine
301
+
302
+ orchs = load_orchestrations()
303
+ orch_data = next((o for o in orchs if o["id"] == orch_id), None)
304
+ if not orch_data:
305
+ raise HTTPException(status_code=404, detail=f"Orchestration '{orch_id}' not found")
306
+
307
+ run_id = f"run_{orch_id}_{int(time.time() * 1000)}"
308
+ orch = Orchestration.model_validate(orch_data)
309
+ engine = OrchestrationEngine(orch, _server)
310
+
311
+ async def event_stream():
312
+ try:
313
+ async for event in engine.run(body.message, run_id):
314
+ etype = event.get("type", "")
315
+
316
+ # Skip internal log events
317
+ if etype.startswith("_log_"):
318
+ continue
319
+
320
+ yield _format_sse_event(event)
321
+
322
+ if etype == "human_input_required":
323
+ yield _format_sse_event({"type": "done"})
324
+ return
325
+
326
+ await asyncio.sleep(0)
327
+ except Exception as e:
328
+ log.exception("[v1/orch/stream] Unhandled error run_id=%s", run_id)
329
+ yield _format_sse_event({"type": "orchestration_error", "error": "An internal error occurred. Check server logs for details."})
330
+
331
+ yield _format_sse_event({"type": "done"})
332
+
333
+ return StreamingResponse(
334
+ event_stream(),
335
+ media_type="text/event-stream",
336
+ headers={
337
+ "Cache-Control": "no-cache",
338
+ "Connection": "keep-alive",
339
+ "X-Accel-Buffering": "no",
340
+ },
341
+ )
342
+
343
+
344
+ # ── Orchestration Resume Endpoints ──────────────────────────────────────────
345
+
346
+ @router.post("/orchestrations/runs/{run_id}/resume")
347
+ async def v1_orchestration_resume(
348
+ run_id: str,
349
+ body: V1ResumeRequest,
350
+ key_record: dict = Depends(require_api_key),
351
+ ):
352
+ """Submit human input and resume orchestration (sync)."""
353
+ import core.server as _server
354
+ from core.orchestration.engine import OrchestrationEngine
355
+
356
+ # Normalize response to dict
357
+ human_response = body.response
358
+ if isinstance(human_response, str):
359
+ human_response = {"response": human_response}
360
+
361
+ final_response = None
362
+ human_input_event = None
363
+ step_history = []
364
+ shared_state = {}
365
+ status = "running"
366
+ _allowed_statuses = {"running", "paused", "completed", "failed"}
367
+
368
+ try:
369
+ async for event in OrchestrationEngine.resume(run_id, human_response, _server):
370
+ etype = event.get("type", "")
371
+
372
+ if etype == "human_input_required":
373
+ human_input_event = event
374
+ status = "paused"
375
+ break
376
+
377
+ if etype == "orchestration_complete":
378
+ _raw_status = event.get("status", "completed")
379
+ status = _raw_status if _raw_status in _allowed_statuses else "completed"
380
+ shared_state = event.get("final_state", {})
381
+
382
+ if etype == "final":
383
+ final_response = event.get("response", "")
384
+ step_history = event.get("data", {}).get("step_history", []) if event.get("data") else []
385
+
386
+ except FileNotFoundError:
387
+ raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
388
+ except Exception as exc:
389
+ log.exception("[v1/resume] Unhandled error run_id=%s", run_id)
390
+ raise HTTPException(status_code=500, detail="An internal error occurred. Check server logs for details.")
391
+
392
+ if human_input_event:
393
+ return {
394
+ "status": "paused",
395
+ "run_id": run_id,
396
+ "human_input_required": {
397
+ "step_id": human_input_event.get("orch_step_id"),
398
+ "prompt": human_input_event.get("prompt"),
399
+ "fields": human_input_event.get("fields", []),
400
+ "agent_context": human_input_event.get("agent_context"),
401
+ },
402
+ }
403
+
404
+ return {
405
+ "status": status,
406
+ "run_id": run_id,
407
+ "response": final_response or f"Orchestration {status}.",
408
+ "shared_state": shared_state,
409
+ "step_history": step_history,
410
+ }
411
+
412
+
413
+ @router.post("/orchestrations/runs/{run_id}/resume/stream")
414
+ async def v1_orchestration_resume_stream(
415
+ run_id: str,
416
+ body: V1ResumeRequest,
417
+ key_record: dict = Depends(require_api_key),
418
+ ):
419
+ """Submit human input and resume orchestration (SSE stream)."""
420
+ import core.server as _server
421
+ from core.orchestration.engine import OrchestrationEngine
422
+
423
+ # Normalize response to dict
424
+ human_response = body.response
425
+ if isinstance(human_response, str):
426
+ human_response = {"response": human_response}
427
+
428
+ async def event_stream():
429
+ try:
430
+ async for event in OrchestrationEngine.resume(run_id, human_response, _server):
431
+ etype = event.get("type", "")
432
+
433
+ if etype.startswith("_log_"):
434
+ continue
435
+
436
+ yield _format_sse_event(event)
437
+
438
+ if etype == "human_input_required":
439
+ yield _format_sse_event({"type": "done"})
440
+ return
441
+
442
+ await asyncio.sleep(0)
443
+ except FileNotFoundError:
444
+ yield _format_sse_event({"type": "orchestration_error", "error": f"Run '{run_id}' not found"})
445
+ except Exception as e:
446
+ log.exception("[v1/resume/stream] Unhandled error run_id=%s", run_id)
447
+ yield _format_sse_event({"type": "orchestration_error", "error": "An internal error occurred. Check server logs for details."})
448
+
449
+ yield _format_sse_event({"type": "done"})
450
+
451
+ return StreamingResponse(
452
+ event_stream(),
453
+ media_type="text/event-stream",
454
+ headers={
455
+ "Cache-Control": "no-cache",
456
+ "Connection": "keep-alive",
457
+ "X-Accel-Buffering": "no",
458
+ },
459
+ )
460
+
461
+
462
+ # ── Read-Only Discovery Endpoints ──────────────────────────────────────────
463
+
464
+ @router.get("/agents")
465
+ async def v1_list_agents(key_record: dict = Depends(require_api_key)):
466
+ """List all configured agents (id, name, type, capabilities)."""
467
+ from core.routes.agents import load_user_agents
468
+ agents = load_user_agents()
469
+ return [
470
+ {
471
+ "id": a["id"],
472
+ "name": a["name"],
473
+ "type": a.get("type", "conversational"),
474
+ "model": a.get("model", ""),
475
+ "capabilities": a.get("capabilities", []),
476
+ }
477
+ for a in agents
478
+ ]
479
+
480
+
481
+ @router.get("/agents/{agent_id}")
482
+ async def v1_get_agent(agent_id: str, key_record: dict = Depends(require_api_key)):
483
+ """Get details for a specific agent."""
484
+ from core.routes.agents import load_user_agents
485
+ agents = load_user_agents()
486
+ agent = next((a for a in agents if a["id"] == agent_id), None)
487
+ if not agent:
488
+ raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
489
+ return {
490
+ "id": agent["id"],
491
+ "name": agent["name"],
492
+ "type": agent.get("type", "conversational"),
493
+ "model": agent.get("model", ""),
494
+ "capabilities": agent.get("capabilities", []),
495
+ "description": agent.get("description", ""),
496
+ }
497
+
498
+
499
+ @router.get("/orchestrations")
500
+ async def v1_list_orchestrations(key_record: dict = Depends(require_api_key)):
501
+ """List all configured orchestrations (id, name, steps summary)."""
502
+ from core.routes.orchestrations import load_orchestrations
503
+ orchs = load_orchestrations()
504
+ return [
505
+ {
506
+ "id": o["id"],
507
+ "name": o.get("name", ""),
508
+ "description": o.get("description", ""),
509
+ "steps": len(o.get("steps", [])),
510
+ }
511
+ for o in orchs
512
+ ]
513
+
514
+
515
+ @router.get("/orchestrations/{orch_id}")
516
+ async def v1_get_orchestration(orch_id: str, key_record: dict = Depends(require_api_key)):
517
+ """Get details for a specific orchestration including step definitions."""
518
+ from core.routes.orchestrations import load_orchestrations
519
+ orchs = load_orchestrations()
520
+ orch = next((o for o in orchs if o["id"] == orch_id), None)
521
+ if not orch:
522
+ raise HTTPException(status_code=404, detail=f"Orchestration '{orch_id}' not found")
523
+ return {
524
+ "id": orch["id"],
525
+ "name": orch.get("name", ""),
526
+ "description": orch.get("description", ""),
527
+ "steps": [
528
+ {
529
+ "id": s.get("id", ""),
530
+ "label": s.get("label", ""),
531
+ "type": s.get("type", ""),
532
+ "agent_id": s.get("agent_id", ""),
533
+ "depends_on": s.get("depends_on", []),
534
+ }
535
+ for s in orch.get("steps", [])
536
+ ],
537
+ "edges": orch.get("edges", []),
538
+ }
539
+
@@ -1,10 +1,13 @@
1
1
  """
2
- Authentication routes (Google OAuth).
2
+ Authentication routes: Google OAuth + Synapse login gate.
3
3
  """
4
4
  import os
5
5
  from fastapi import APIRouter, HTTPException
6
6
  from fastapi.responses import RedirectResponse, JSONResponse
7
+ from pydantic import BaseModel
7
8
  from services.google import get_auth_url, finish_auth
9
+ from core.config import load_settings
10
+ from core.user_auth import verify_password, create_session_token
8
11
 
9
12
  router = APIRouter()
10
13
 
@@ -28,6 +31,45 @@ async def login():
28
31
  raise HTTPException(status_code=500, detail=str(e))
29
32
 
30
33
 
34
+ class LoginRequest(BaseModel):
35
+ username: str
36
+ password: str
37
+
38
+
39
+ @router.get("/api/auth/status")
40
+ async def auth_status():
41
+ """Returns whether login is enabled and fully configured."""
42
+ s = load_settings()
43
+ login_enabled = s.get("login_enabled", False)
44
+ login_configured = bool(
45
+ login_enabled
46
+ and s.get("login_username")
47
+ and s.get("login_password_hash")
48
+ )
49
+ return {"login_enabled": login_enabled, "login_configured": login_configured}
50
+
51
+
52
+ @router.post("/api/auth/login")
53
+ async def user_login(body: LoginRequest):
54
+ """Validate username/password and return a signed JWT on success."""
55
+ s = load_settings()
56
+ if not s.get("login_enabled"):
57
+ return {"success": True, "token": None}
58
+ stored_username = s.get("login_username", "")
59
+ stored_hash = s.get("login_password_hash", "")
60
+ if not (stored_username and stored_hash):
61
+ raise HTTPException(status_code=500, detail="Login is enabled but credentials are not configured")
62
+ if body.username != stored_username or not verify_password(body.password, stored_hash):
63
+ raise HTTPException(status_code=401, detail="Invalid username or password")
64
+ return {"success": True, "token": create_session_token(body.username)}
65
+
66
+
67
+ @router.post("/api/auth/logout")
68
+ async def user_logout():
69
+ """Stateless logout — cookie cleared by the Next.js route handler."""
70
+ return {"success": True}
71
+
72
+
31
73
  @router.get("/auth/callback")
32
74
  async def callback(code: str, state: str = None):
33
75
  try:
@@ -28,6 +28,12 @@ class EmbedSetupRequest(BaseModel):
28
28
  password: str = ""
29
29
  db_name: str = "synapse"
30
30
 
31
+
32
+ class LoginConfigRequest(BaseModel):
33
+ login_enabled: bool
34
+ login_username: str = ""
35
+ login_password: str = ""
36
+
31
37
  # Path to the examples directory (sibling of this file's package root)
32
38
  _EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "examples")
33
39
 
@@ -87,19 +93,40 @@ async def get_status():
87
93
  @router.get("/api/settings")
88
94
  async def get_settings():
89
95
  settings = load_settings()
96
+ settings.pop("login_password_hash", None)
90
97
  return settings
91
98
 
92
99
 
100
+ @router.post("/api/settings/login")
101
+ async def update_login_settings(body: LoginConfigRequest):
102
+ from core.user_auth import hash_password
103
+ existing = load_settings()
104
+ existing["login_enabled"] = body.login_enabled
105
+ if body.login_enabled:
106
+ if not body.login_username.strip():
107
+ raise HTTPException(status_code=400, detail="Username is required when login is enabled")
108
+ existing["login_username"] = body.login_username.strip()
109
+ if body.login_password:
110
+ existing["login_password_hash"] = hash_password(body.login_password)
111
+ elif not existing.get("login_password_hash"):
112
+ raise HTTPException(status_code=400, detail="Password is required for first-time login setup")
113
+ save_settings(existing)
114
+ return {"status": "ok", "login_enabled": body.login_enabled}
115
+
116
+
93
117
  @router.post("/api/settings")
94
118
  async def update_settings(settings: Settings):
95
- print(f"DEBUG: update_settings called with: {settings.dict()}")
119
+ _safe = {k: (v[:4] + '…' + v[-4:] if isinstance(v, str) and len(v) > 12 and any(kw in k for kw in ('key', 'token', 'secret', 'password')) else v) for k, v in settings.dict(exclude_unset=True).items()}
120
+ print(f"DEBUG: update_settings called with keys: {list(_safe.keys())}")
96
121
  # Get the latest payload and strip unset values to avoid overwriting existing properties with defaults
97
122
  try:
98
123
  data = settings.dict(exclude_unset=True)
99
124
  except Exception:
100
125
  data = settings.dict()
101
-
126
+
102
127
  existing = load_settings()
128
+ # Never overwrite the bcrypt hash via the general settings endpoint
129
+ data.pop("login_password_hash", None)
103
130
  existing.update(data)
104
131
  data = existing
105
132