beru 1.0.0a1__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 (254) hide show
  1. beru/__init__.py +0 -0
  2. beru/api/__init__.py +1 -0
  3. beru/api/v1/__init__.py +1 -0
  4. beru/api/v1/agent_sessions.py +370 -0
  5. beru/api/v1/agents.py +582 -0
  6. beru/api/v1/app_auth.py +160 -0
  7. beru/api/v1/beru_models.py +53 -0
  8. beru/api/v1/beru_services.py +58 -0
  9. beru/api/v1/browser.py +187 -0
  10. beru/api/v1/cloud/__init__.py +2 -0
  11. beru/api/v1/cloud/auth.py +232 -0
  12. beru/api/v1/cloud/billing.py +156 -0
  13. beru/api/v1/cloud/sync.py +125 -0
  14. beru/api/v1/email_settings.py +185 -0
  15. beru/api/v1/endpoints/__init__.py +0 -0
  16. beru/api/v1/endpoints/health.py +8 -0
  17. beru/api/v1/execution_threads.py +770 -0
  18. beru/api/v1/executions.py +439 -0
  19. beru/api/v1/health.py +46 -0
  20. beru/api/v1/host.py +118 -0
  21. beru/api/v1/inbound.py +115 -0
  22. beru/api/v1/instance.py +61 -0
  23. beru/api/v1/mcp_servers.py +630 -0
  24. beru/api/v1/providers.py +334 -0
  25. beru/api/v1/push.py +65 -0
  26. beru/api/v1/router.py +250 -0
  27. beru/api/v1/schedules.py +185 -0
  28. beru/api/v1/secrets.py +32 -0
  29. beru/api/v1/session.py +346 -0
  30. beru/api/v1/starters.py +65 -0
  31. beru/api/v1/users.py +108 -0
  32. beru/api/v1/workflows.py +544 -0
  33. beru/api/v1/workspace_files.py +146 -0
  34. beru/api/workspace_proxy.py +222 -0
  35. beru/cli.py +417 -0
  36. beru/core/__init__.py +0 -0
  37. beru/core/access.py +198 -0
  38. beru/core/account_deletion.py +143 -0
  39. beru/core/account_mail.py +274 -0
  40. beru/core/accounts.py +181 -0
  41. beru/core/app_tokens.py +209 -0
  42. beru/core/clis.py +162 -0
  43. beru/core/config.py +258 -0
  44. beru/core/database.py +442 -0
  45. beru/core/dependencies.py +115 -0
  46. beru/core/deployment.py +45 -0
  47. beru/core/environment.py +223 -0
  48. beru/core/exceptions.py +638 -0
  49. beru/core/features.py +224 -0
  50. beru/core/host.py +212 -0
  51. beru/core/llm_catalog.py +404 -0
  52. beru/core/local_runner.py +144 -0
  53. beru/core/mcp_seed.py +215 -0
  54. beru/core/members.py +139 -0
  55. beru/core/model_registry.py +338 -0
  56. beru/core/public.py +107 -0
  57. beru/core/security.py +249 -0
  58. beru/core/uuid.py +18 -0
  59. beru/crud/__init__.py +0 -0
  60. beru/data/models_dev.json +9410 -0
  61. beru/king_beru.py +268 -0
  62. beru/main.py +544 -0
  63. beru/middleware/basic_auth.py +295 -0
  64. beru/middleware/error_handler.py +430 -0
  65. beru/models/__init__.py +23 -0
  66. beru/models/account_token.py +19 -0
  67. beru/models/agent.py +46 -0
  68. beru/models/agent_session.py +56 -0
  69. beru/models/app_session.py +41 -0
  70. beru/models/base.py +35 -0
  71. beru/models/chat_secret.py +21 -0
  72. beru/models/document.py +28 -0
  73. beru/models/email_outbox.py +23 -0
  74. beru/models/email_verification.py +28 -0
  75. beru/models/event_log.py +101 -0
  76. beru/models/execution.py +57 -0
  77. beru/models/execution_thread.py +39 -0
  78. beru/models/instance_setting.py +18 -0
  79. beru/models/llm_model.py +36 -0
  80. beru/models/mcp_server.py +121 -0
  81. beru/models/push_subscription.py +24 -0
  82. beru/models/schedule.py +30 -0
  83. beru/models/stripe_event.py +34 -0
  84. beru/models/user.py +84 -0
  85. beru/models/workflow.py +69 -0
  86. beru/models/workspace.py +84 -0
  87. beru/runner/__init__.py +18 -0
  88. beru/runner/__main__.py +5 -0
  89. beru/runner/config.py +103 -0
  90. beru/runner/core.py +1071 -0
  91. beru/runner/cron.py +138 -0
  92. beru/runner/engines.py +300 -0
  93. beru/runner/harness.py +139 -0
  94. beru/runner/mcp.py +254 -0
  95. beru/runner/procs.py +149 -0
  96. beru/runner/server.py +266 -0
  97. beru/schemas/__init__.py +64 -0
  98. beru/schemas/agent.py +115 -0
  99. beru/schemas/agent_session.py +147 -0
  100. beru/schemas/auth.py +87 -0
  101. beru/schemas/base.py +28 -0
  102. beru/schemas/billing.py +79 -0
  103. beru/schemas/document.py +75 -0
  104. beru/schemas/execution.py +126 -0
  105. beru/schemas/execution_thread.py +78 -0
  106. beru/schemas/llm_model.py +100 -0
  107. beru/schemas/mcp_server.py +134 -0
  108. beru/schemas/sync.py +78 -0
  109. beru/schemas/workflow.py +159 -0
  110. beru/services/__init__.py +7 -0
  111. beru/services/agent_run_tracker.py +159 -0
  112. beru/services/agent_runner.py +329 -0
  113. beru/services/agent_service.py +1107 -0
  114. beru/services/agent_session_service.py +145 -0
  115. beru/services/auth_service.py +370 -0
  116. beru/services/base.py +245 -0
  117. beru/services/beru_models.py +302 -0
  118. beru/services/beru_services.py +269 -0
  119. beru/services/billing_service.py +492 -0
  120. beru/services/chat_secrets.py +143 -0
  121. beru/services/compaction.py +264 -0
  122. beru/services/context_budget.py +133 -0
  123. beru/services/conversation_service.py +1031 -0
  124. beru/services/document_service.py +391 -0
  125. beru/services/duplication.py +123 -0
  126. beru/services/email_service.py +280 -0
  127. beru/services/error_reports.py +212 -0
  128. beru/services/event_log_service.py +249 -0
  129. beru/services/execution_engine.py +1010 -0
  130. beru/services/execution_service.py +897 -0
  131. beru/services/execution_thread_service.py +265 -0
  132. beru/services/execution_types.py +39 -0
  133. beru/services/knowledge_base.py +465 -0
  134. beru/services/llm_provider_service.py +412 -0
  135. beru/services/llm_service.py +273 -0
  136. beru/services/mcp_integration.py +605 -0
  137. beru/services/mcp_oauth.py +441 -0
  138. beru/services/mcp_server_service.py +1684 -0
  139. beru/services/monitor.py +399 -0
  140. beru/services/monitoring.py +676 -0
  141. beru/services/nodes/__init__.py +34 -0
  142. beru/services/nodes/agent_handler.py +582 -0
  143. beru/services/nodes/base.py +109 -0
  144. beru/services/nodes/condition_handler.py +267 -0
  145. beru/services/nodes/constant_handler.py +94 -0
  146. beru/services/nodes/control_handler.py +650 -0
  147. beru/services/nodes/external_agent_handler.py +385 -0
  148. beru/services/nodes/flow_handler.py +365 -0
  149. beru/services/nodes/tool_handler.py +612 -0
  150. beru/services/prompt_manager.py +411 -0
  151. beru/services/push.py +272 -0
  152. beru/services/rag_service.py +400 -0
  153. beru/services/run_registry.py +263 -0
  154. beru/services/run_user.py +43 -0
  155. beru/services/schedules.py +395 -0
  156. beru/services/session_turn.py +279 -0
  157. beru/services/state_manager.py +337 -0
  158. beru/services/sync_service.py +359 -0
  159. beru/services/task_runner.py +287 -0
  160. beru/services/titles.py +155 -0
  161. beru/services/tools/__init__.py +16 -0
  162. beru/services/tools/admin.py +480 -0
  163. beru/services/tools/ask.py +103 -0
  164. beru/services/tools/deferred.py +191 -0
  165. beru/services/tools/live_sessions.py +331 -0
  166. beru/services/tools/native.py +306 -0
  167. beru/services/tools/report.py +189 -0
  168. beru/services/tools/runner_tools.py +291 -0
  169. beru/services/tools/schedules.py +272 -0
  170. beru/services/tools/share.py +52 -0
  171. beru/services/tools/workspace.py +421 -0
  172. beru/services/uploads.py +356 -0
  173. beru/services/workflow_service.py +1293 -0
  174. beru/services/workflow_validator.py +547 -0
  175. beru/starters/__init__.py +576 -0
  176. beru/starters/agents/architect.json +13 -0
  177. beru/starters/agents/author.json +13 -0
  178. beru/starters/agents/closer.json +13 -0
  179. beru/starters/agents/copywriter.json +13 -0
  180. beru/starters/agents/dealdesk.json +13 -0
  181. beru/starters/agents/developer.json +10 -0
  182. beru/starters/agents/editor.json +16 -0
  183. beru/starters/agents/generalist.json +10 -0
  184. beru/starters/agents/integrator.json +13 -0
  185. beru/starters/agents/judge.json +13 -0
  186. beru/starters/agents/marketing-editor.json +13 -0
  187. beru/starters/agents/planner.json +13 -0
  188. beru/starters/agents/qa-reviewer.json +17 -0
  189. beru/starters/agents/researcher.json +13 -0
  190. beru/starters/agents/reviewer-clarity.json +13 -0
  191. beru/starters/agents/reviewer-correctness.json +13 -0
  192. beru/starters/agents/reviewer-risk.json +13 -0
  193. beru/starters/agents/reviewer.json +16 -0
  194. beru/starters/agents/scout.json +13 -0
  195. beru/starters/agents/spec-writer.json +13 -0
  196. beru/starters/agents/strategist.json +13 -0
  197. beru/starters/agents/tech-lead.json +16 -0
  198. beru/starters/workflows/bizdev-team.json +192 -0
  199. beru/starters/workflows/dev-team.json +152 -0
  200. beru/starters/workflows/marketing-team.json +152 -0
  201. beru/starters/workflows/plan-delegate-integrate.json +119 -0
  202. beru/starters/workflows/react-baseline.json +53 -0
  203. beru/starters/workflows/review-panel.json +134 -0
  204. beru/starters/workflows/reviewed-answer.json +131 -0
  205. beru/starters/workflows/second-opinion.json +105 -0
  206. beru/starters/workflows/workspace-handoff.json +95 -0
  207. beru/static/ui/android-chrome-192x192.png +0 -0
  208. beru/static/ui/android-chrome-512x512.png +0 -0
  209. beru/static/ui/apple-touch-icon.png +0 -0
  210. beru/static/ui/assets/html2canvas.esm-CBrSDip1.js +22 -0
  211. beru/static/ui/assets/index-8V-nI-TB.js +918 -0
  212. beru/static/ui/assets/index-DgIeHdmD.css +1 -0
  213. beru/static/ui/assets/index.es-D2jYrrlu.js +18 -0
  214. beru/static/ui/assets/jspdf.es.min-D32WELE7.js +170 -0
  215. beru/static/ui/assets/jspdf.plugin.autotable-DYHf0IZ0.js +2 -0
  216. beru/static/ui/assets/pdf-BnM3EHEC.js +4 -0
  217. beru/static/ui/assets/purify.es-CAi2OJFm.js +3 -0
  218. beru/static/ui/assets/server.browser-BtnbRC1f.js +27 -0
  219. beru/static/ui/assets/svg2pdf.es.min-DANJbn6A.js +2 -0
  220. beru/static/ui/beru-crest.png +0 -0
  221. beru/static/ui/favicon-16x16.png +0 -0
  222. beru/static/ui/favicon-32x32.png +0 -0
  223. beru/static/ui/favicon.ico +0 -0
  224. beru/static/ui/index.html +50 -0
  225. beru/static/ui/maskable-icon-512x512.png +0 -0
  226. beru/static/ui/placeholder.svg +1 -0
  227. beru/static/ui/robots.txt +14 -0
  228. beru/static/ui/site.webmanifest +74 -0
  229. beru/static/ui/splash/apple-splash-1125x2436.png +0 -0
  230. beru/static/ui/splash/apple-splash-1170x2532.png +0 -0
  231. beru/static/ui/splash/apple-splash-1179x2556.png +0 -0
  232. beru/static/ui/splash/apple-splash-1206x2622.png +0 -0
  233. beru/static/ui/splash/apple-splash-1242x2688.png +0 -0
  234. beru/static/ui/splash/apple-splash-1284x2778.png +0 -0
  235. beru/static/ui/splash/apple-splash-1290x2796.png +0 -0
  236. beru/static/ui/splash/apple-splash-1320x2868.png +0 -0
  237. beru/static/ui/splash/apple-splash-1536x2048.png +0 -0
  238. beru/static/ui/splash/apple-splash-1640x2360.png +0 -0
  239. beru/static/ui/splash/apple-splash-1668x2388.png +0 -0
  240. beru/static/ui/splash/apple-splash-2048x2732.png +0 -0
  241. beru/static/ui/splash/apple-splash-750x1334.png +0 -0
  242. beru/static/ui/splash/apple-splash-828x1792.png +0 -0
  243. beru/static/ui/sw.js +114 -0
  244. beru/utils/__init__.py +0 -0
  245. beru/utils/embedding_utils.py +247 -0
  246. beru/utils/llm_clients.py +584 -0
  247. beru/utils/serialization.py +86 -0
  248. beru/utils/text_processing.py +428 -0
  249. beru-1.0.0a1.dist-info/METADATA +91 -0
  250. beru-1.0.0a1.dist-info/RECORD +254 -0
  251. beru-1.0.0a1.dist-info/WHEEL +5 -0
  252. beru-1.0.0a1.dist-info/entry_points.txt +3 -0
  253. beru-1.0.0a1.dist-info/licenses/LICENSE +21 -0
  254. beru-1.0.0a1.dist-info/top_level.txt +1 -0
beru/__init__.py ADDED
File without changes
beru/api/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # API package initialization
@@ -0,0 +1 @@
1
+ # API v1 package initialization
@@ -0,0 +1,370 @@
1
+ """
2
+ Agent chat session CRUD endpoints.
3
+
4
+ Routes
5
+ ------
6
+ GET /agent-sessions/ — list all sessions (optionally filtered by agent_id)
7
+ POST /agent-sessions/ — create a session (optionally on another model)
8
+ GET /agent-sessions/models?agent_id= — the models a chat may run on, and the agent's for this person
9
+ PUT /agent-sessions/{id}/model — switch the chat to another model, or back to its agent's
10
+ GET /agent-sessions/{id} — get session + messages
11
+ PUT /agent-sessions/{id} — rename session
12
+ DELETE /agent-sessions/{id} — delete session (cascades to messages)
13
+ GET /agent-sessions/{id}/messages — list messages only
14
+ POST /agent-sessions/{id}/compact — fold older turns into a summary now
15
+ POST /agent-sessions/{id}/title — name it from its first prompt (beru/services/titles.py)
16
+ POST /agent-sessions/{id}/uploads — attach files (multipart `files`); see beru/services/uploads.py
17
+ DELETE /agent-sessions/{id}/uploads/{name} — remove an attachment that was not sent after all
18
+ """
19
+
20
+ import logging
21
+ from typing import List, Optional
22
+
23
+ from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
24
+ from fastapi.concurrency import run_in_threadpool
25
+ from sqlalchemy.orm import Session
26
+
27
+ from beru.core.access import Viewer, get_viewer
28
+ from beru.core.database import get_db
29
+ from beru.schemas.agent_session import (
30
+ AgentSessionCreate,
31
+ AgentSessionResponse,
32
+ AgentSessionUpdate,
33
+ AgentSessionWithMessages,
34
+ AgentMessageResponse,
35
+ Attachment,
36
+ ChatModelOption,
37
+ ChatModelProvider,
38
+ ChatModels,
39
+ CompactionResult,
40
+ ModelChoice,
41
+ TitleRequest,
42
+ model_choice,
43
+ session_context,
44
+ )
45
+ from beru.services.agent_session_service import AgentSessionService
46
+
47
+ logger = logging.getLogger(__name__)
48
+ router = APIRouter(prefix="/agent-sessions", tags=["agent-sessions"])
49
+
50
+
51
+ @router.get("/", response_model=List[AgentSessionResponse])
52
+ async def list_agent_sessions(
53
+ agent_id: Optional[str] = Query(None, description="Filter by agent ID"),
54
+ db: Session = Depends(get_db),
55
+ viewer: Viewer = Depends(get_viewer),
56
+ ):
57
+ """List all agent chat sessions, optionally filtered by agent_id."""
58
+ return [AgentSessionResponse.from_row(s) for s in AgentSessionService(db, viewer).list_sessions(agent_id=agent_id)]
59
+
60
+
61
+ @router.post("/", response_model=AgentSessionResponse, status_code=status.HTTP_201_CREATED)
62
+ async def create_agent_session(
63
+ body: AgentSessionCreate,
64
+ db: Session = Depends(get_db),
65
+ viewer: Viewer = Depends(get_viewer),
66
+ ):
67
+ """Create a new agent chat session."""
68
+ choice = _usable_choice(db, viewer, body.model) if body.model and body.model.provider_id else None
69
+ svc = AgentSessionService(db, viewer)
70
+ session = svc.create_session(agent_id=body.agent_id, name=body.name)
71
+ if choice:
72
+ session = svc.set_model(session.id, choice)
73
+ return AgentSessionResponse.from_row(session)
74
+
75
+
76
+ def _usable_choice(db: Session, viewer: Viewer, body: ModelChoice) -> dict:
77
+ """{"provider_id", "model"} when this person may run on it (AgentService.usable_providers), else 400."""
78
+ from beru.services.agent_service import AgentService
79
+
80
+ provider = AgentService(db).usable(body.provider_id, body.model, viewer.user_id, viewer.manager)
81
+ if provider is None:
82
+ raise HTTPException(status_code=400, detail="That is not a model you can use: pick one from your providers")
83
+ return {"provider_id": str(provider.id), "model": body.model}
84
+
85
+
86
+ @router.get("/models", response_model=ChatModels)
87
+ async def chat_models(
88
+ agent_id: Optional[str] = Query(None, description="Also say what this agent runs on for the caller"),
89
+ db: Session = Depends(get_db),
90
+ viewer: Viewer = Depends(get_viewer),
91
+ ):
92
+ """
93
+ Every model a chat may be switched to: those of the providers this person may run on (the
94
+ workspace's and their own; a public instance's members, their own), oldest provider first, with
95
+ `default`, what `agent_id` runs on for them when the chat is not switched.
96
+ """
97
+ from beru.services import beru_models
98
+ from beru.services.agent_service import AgentService
99
+
100
+ service = AgentService(db)
101
+ providers = []
102
+ for p in service.usable_providers(viewer.user_id, viewer.manager):
103
+ labels = beru_models.MODEL_LABELS if p.name == beru_models.NAME else {}
104
+ providers.append(ChatModelProvider(
105
+ provider_id=str(p.id), name=p.display_name or p.name, provider_type=str(p.provider_type),
106
+ personal=p.owner_user_id is not None,
107
+ models=[ChatModelOption(id=m, label=labels.get(m, (None, None))[0], vendor=labels.get(m, (None, None))[1])
108
+ for m in p.supported_models],
109
+ ))
110
+ out = ChatModels(providers=providers)
111
+ agent = await service.get_agent(agent_id) if agent_id else None
112
+ if agent is not None:
113
+ out.default_yours = service.person_model(agent.id, viewer.user_id, viewer.manager) is not None
114
+ try:
115
+ ran, provider = await service.provider_for(agent, viewer.user_id, viewer.manager)
116
+ if provider is not None:
117
+ out.default = ModelChoice(provider_id=str(provider.id), model=ran.model)
118
+ except ValueError as e:
119
+ out.default_error = str(e)
120
+ return out
121
+
122
+
123
+ @router.get("/{session_id}", response_model=AgentSessionWithMessages)
124
+ async def get_agent_session(
125
+ session_id: str,
126
+ db: Session = Depends(get_db),
127
+ viewer: Viewer = Depends(get_viewer),
128
+ ):
129
+ """Get a session with its full message history."""
130
+ svc = AgentSessionService(db, viewer)
131
+ session = svc.get_session(session_id)
132
+ if not session:
133
+ raise HTTPException(status_code=404, detail="Session not found")
134
+
135
+ msgs = svc.get_messages(session_id)
136
+ return AgentSessionWithMessages(
137
+ id=session.id,
138
+ agent_id=session.agent_id,
139
+ name=session.name,
140
+ created_at=session.created_at,
141
+ updated_at=session.updated_at,
142
+ context=session_context(session.meta),
143
+ title_source=(session.meta or {}).get("title_source"),
144
+ model=model_choice(session.meta),
145
+ messages=[
146
+ AgentMessageResponse(
147
+ id=m.id,
148
+ session_id=m.session_id,
149
+ role=m.role,
150
+ content=m.content,
151
+ tool_name=m.tool_name,
152
+ meta=m.meta,
153
+ created_at=m.created_at,
154
+ updated_at=m.updated_at,
155
+ )
156
+ for m in msgs
157
+ ],
158
+ )
159
+
160
+
161
+ @router.put("/{session_id}", response_model=AgentSessionResponse)
162
+ async def rename_agent_session(
163
+ session_id: str,
164
+ body: AgentSessionUpdate,
165
+ db: Session = Depends(get_db),
166
+ viewer: Viewer = Depends(get_viewer),
167
+ ):
168
+ """Rename a session."""
169
+ session = AgentSessionService(db, viewer).rename_session(session_id, body.name)
170
+ if not session:
171
+ raise HTTPException(status_code=404, detail="Session not found")
172
+ return AgentSessionResponse.from_row(session)
173
+
174
+
175
+ @router.put("/{session_id}/model", response_model=AgentSessionResponse)
176
+ async def set_agent_session_model(
177
+ session_id: str,
178
+ body: ModelChoice,
179
+ db: Session = Depends(get_db),
180
+ viewer: Viewer = Depends(get_viewer),
181
+ ):
182
+ """Run the chat's next turns on another model this person may use, or (both fields empty) on its
183
+ agent's own again. The agent itself is unchanged, for everyone else too."""
184
+ svc = AgentSessionService(db, viewer)
185
+ if not svc.get_session(session_id):
186
+ raise HTTPException(status_code=404, detail="Session not found")
187
+ choice = _usable_choice(db, viewer, body) if body.provider_id or body.model else None
188
+ return AgentSessionResponse.from_row(svc.set_model(session_id, choice))
189
+
190
+
191
+ @router.post("/{session_id}/title", response_model=AgentSessionResponse)
192
+ async def title_agent_session(
193
+ session_id: str,
194
+ body: Optional[TitleRequest] = None,
195
+ db: Session = Depends(get_db),
196
+ viewer: Viewer = Depends(get_viewer),
197
+ ):
198
+ """Name the session from its first prompt. A name the user typed stays unless `force`."""
199
+ from beru.services import titles
200
+
201
+ body = body or TitleRequest()
202
+ svc = AgentSessionService(db, viewer)
203
+ session = svc.get_session(session_id)
204
+ if not session:
205
+ raise HTTPException(status_code=404, detail="Session not found")
206
+ if (session.meta or {}).get("title_source") == "user" and not body.force:
207
+ return AgentSessionResponse.from_row(session)
208
+ prompt = body.prompt
209
+ if not prompt:
210
+ first = next((m for m in svc.get_messages(session_id) if m.role == "user"), None)
211
+ inbound = ((first.meta or {}).get("inbound") or {}) if first else {}
212
+ prompt = inbound.get("body") or (first.content if first else "")
213
+ if not (prompt or "").strip():
214
+ raise HTTPException(status_code=409, detail="Nothing to name it from yet: the session has no messages")
215
+ title = await titles.generate(prompt)
216
+ session = svc.rename_session(session_id, title.text, source=title.source)
217
+ return AgentSessionResponse.from_row(session)
218
+
219
+
220
+ @router.delete("/{session_id}", status_code=status.HTTP_204_NO_CONTENT)
221
+ async def delete_agent_session(
222
+ session_id: str,
223
+ db: Session = Depends(get_db),
224
+ viewer: Viewer = Depends(get_viewer),
225
+ ):
226
+ """Delete a session, its messages, and the files attached in it."""
227
+ if not AgentSessionService(db, viewer).delete_session(session_id):
228
+ raise HTTPException(status_code=404, detail="Session not found")
229
+
230
+
231
+ @router.post("/{session_id}/uploads", response_model=List[Attachment], status_code=status.HTTP_201_CREATED)
232
+ async def upload_to_agent_session(
233
+ session_id: str,
234
+ files: List[UploadFile] = File(...),
235
+ db: Session = Depends(get_db),
236
+ viewer: Viewer = Depends(get_viewer),
237
+ ):
238
+ """
239
+ Save files into the session's uploads folder in the workspace. They are attached to a message by
240
+ sending their names with it (`input_data.attachments`), and deleted with the session.
241
+ """
242
+ from beru.core import public
243
+ from beru.core.config import settings
244
+ from beru.services import uploads
245
+
246
+ if not AgentSessionService(db, viewer).get_session(session_id):
247
+ raise HTTPException(status_code=404, detail="Session not found")
248
+ # Agents read attachments with their workspace tools, which a public instance's members do not have.
249
+ public.not_for_members(viewer, "attaching files")
250
+ if len(files) > settings.CHAT_UPLOAD_MAX_FILES:
251
+ raise HTTPException(status_code=400, detail=f"At most {settings.CHAT_UPLOAD_MAX_FILES} files at a time")
252
+ saved: List[dict] = []
253
+ try:
254
+ for f in files:
255
+ saved.append(await run_in_threadpool(uploads.save, db, session_id, f.filename, f.file, f.content_type))
256
+ except uploads.UploadError as e:
257
+ # All or nothing: the files saved before the refused one would never be sent.
258
+ for a in saved:
259
+ uploads.delete_file(session_id, a["name"])
260
+ raise HTTPException(status_code=413 if "limit" in str(e) else 400, detail=str(e))
261
+ return saved
262
+
263
+
264
+ @router.get("/{session_id}/files/{file_path:path}")
265
+ async def get_agent_session_file(
266
+ session_id: str,
267
+ file_path: str,
268
+ download: bool = False,
269
+ db: Session = Depends(get_db),
270
+ viewer: Viewer = Depends(get_viewer),
271
+ ):
272
+ """
273
+ A file of this chat: one the person attached, or one an agent shared (`shared/<name>`). Only to
274
+ whoever may see the chat. Pages and SVGs are served sandboxed (their own opaque origin, no
275
+ access to this app or its cookies); `?download=1` asks the browser to save it.
276
+ """
277
+ import mimetypes
278
+
279
+ from fastapi.responses import FileResponse
280
+
281
+ from beru.services import uploads
282
+
283
+ if not AgentSessionService(db, viewer).get_session(session_id):
284
+ raise HTTPException(status_code=404, detail="Session not found")
285
+ try:
286
+ target = uploads.session_file(session_id, file_path)
287
+ except uploads.UploadError:
288
+ target = None
289
+ if target is None:
290
+ raise HTTPException(status_code=404, detail="File not found")
291
+ media = mimetypes.guess_type(target.name)[0] or "application/octet-stream"
292
+ if target.suffix.lower() in (".md", ".markdown", ".csv", ".txt", ".log", ".json", ".yaml", ".yml", ".py", ".ts", ".tsx", ".js"):
293
+ media = "text/plain; charset=utf-8" if not media.startswith("text/") or "charset" not in media else media
294
+ headers = {"X-Content-Type-Options": "nosniff", "Cache-Control": "private, max-age=3600"}
295
+ if media.split(";")[0] in ("text/html", "application/xhtml+xml", "image/svg+xml", "application/xml", "text/xml"):
296
+ headers["Content-Security-Policy"] = "sandbox allow-scripts allow-popups allow-downloads"
297
+ return FileResponse(target, media_type=media, headers=headers,
298
+ filename=target.name if download else None,
299
+ content_disposition_type="attachment" if download else "inline")
300
+
301
+
302
+ @router.delete("/{session_id}/uploads/{name}", status_code=status.HTTP_204_NO_CONTENT)
303
+ async def delete_agent_session_upload(
304
+ session_id: str,
305
+ name: str,
306
+ db: Session = Depends(get_db),
307
+ viewer: Viewer = Depends(get_viewer),
308
+ ):
309
+ """Remove one attachment (the chip taken off the message before it was sent)."""
310
+ from beru.services import uploads
311
+
312
+ if not AgentSessionService(db, viewer).get_session(session_id):
313
+ raise HTTPException(status_code=404, detail="Session not found")
314
+ if not uploads.delete_file(session_id, name):
315
+ raise HTTPException(status_code=404, detail="Attachment not found")
316
+
317
+
318
+ @router.get("/{session_id}/messages", response_model=List[AgentMessageResponse])
319
+ async def get_agent_session_messages(
320
+ session_id: str,
321
+ db: Session = Depends(get_db),
322
+ viewer: Viewer = Depends(get_viewer),
323
+ ):
324
+ """List all messages in a session."""
325
+ svc = AgentSessionService(db, viewer)
326
+ if not svc.get_session(session_id):
327
+ raise HTTPException(status_code=404, detail="Session not found")
328
+ return svc.get_messages(session_id)
329
+
330
+
331
+ @router.post("/{session_id}/compact", response_model=CompactionResult)
332
+ async def compact_agent_session(
333
+ session_id: str,
334
+ db: Session = Depends(get_db),
335
+ viewer: Viewer = Depends(get_viewer),
336
+ ):
337
+ """
338
+ Compact the session now: everything but the latest turn is folded into a summary by the
339
+ session's own model (see services.compaction). Automatic compaction does the same when a
340
+ session grows past its context budget; this is for doing it early, or after a long detour.
341
+ """
342
+ from beru.services.agent_service import AgentService
343
+ from beru.services.compaction import compact_session
344
+
345
+ svc = AgentSessionService(db, viewer)
346
+ session = svc.get_session(session_id)
347
+ if not session:
348
+ raise HTTPException(status_code=404, detail="Session not found")
349
+ service = AgentService(db)
350
+ agent = await service.get_agent(str(session.agent_id))
351
+ try:
352
+ agent, provider = await service.provider_for(agent, viewer.user_id, viewer.manager, session_id) if agent else (None, None)
353
+ except ValueError as e:
354
+ raise HTTPException(status_code=409, detail=str(e))
355
+ if agent is None or provider is None:
356
+ raise HTTPException(status_code=409, detail="The session's agent or provider no longer exists")
357
+ from beru.core.access import connection_visible
358
+
359
+ if not connection_visible(provider.owner_user_id, viewer):
360
+ raise HTTPException(status_code=409, detail="This agent runs on someone's personal provider")
361
+ budget = AgentService.context_budget(agent, provider)
362
+ try:
363
+ result = await compact_session(db, session_id, agent, provider, budget, keep_recent_turns=1, force=True)
364
+ except Exception as e: # noqa: BLE001
365
+ raise HTTPException(status_code=502, detail=f"Compaction failed: {e}")
366
+ if result is None:
367
+ raise HTTPException(status_code=409, detail="Nothing to compact yet: the session needs more than one turn")
368
+ db.refresh(session)
369
+ return CompactionResult(**result, context=session_context(session.meta))
370
+