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.
- beru/__init__.py +0 -0
- beru/api/__init__.py +1 -0
- beru/api/v1/__init__.py +1 -0
- beru/api/v1/agent_sessions.py +370 -0
- beru/api/v1/agents.py +582 -0
- beru/api/v1/app_auth.py +160 -0
- beru/api/v1/beru_models.py +53 -0
- beru/api/v1/beru_services.py +58 -0
- beru/api/v1/browser.py +187 -0
- beru/api/v1/cloud/__init__.py +2 -0
- beru/api/v1/cloud/auth.py +232 -0
- beru/api/v1/cloud/billing.py +156 -0
- beru/api/v1/cloud/sync.py +125 -0
- beru/api/v1/email_settings.py +185 -0
- beru/api/v1/endpoints/__init__.py +0 -0
- beru/api/v1/endpoints/health.py +8 -0
- beru/api/v1/execution_threads.py +770 -0
- beru/api/v1/executions.py +439 -0
- beru/api/v1/health.py +46 -0
- beru/api/v1/host.py +118 -0
- beru/api/v1/inbound.py +115 -0
- beru/api/v1/instance.py +61 -0
- beru/api/v1/mcp_servers.py +630 -0
- beru/api/v1/providers.py +334 -0
- beru/api/v1/push.py +65 -0
- beru/api/v1/router.py +250 -0
- beru/api/v1/schedules.py +185 -0
- beru/api/v1/secrets.py +32 -0
- beru/api/v1/session.py +346 -0
- beru/api/v1/starters.py +65 -0
- beru/api/v1/users.py +108 -0
- beru/api/v1/workflows.py +544 -0
- beru/api/v1/workspace_files.py +146 -0
- beru/api/workspace_proxy.py +222 -0
- beru/cli.py +417 -0
- beru/core/__init__.py +0 -0
- beru/core/access.py +198 -0
- beru/core/account_deletion.py +143 -0
- beru/core/account_mail.py +274 -0
- beru/core/accounts.py +181 -0
- beru/core/app_tokens.py +209 -0
- beru/core/clis.py +162 -0
- beru/core/config.py +258 -0
- beru/core/database.py +442 -0
- beru/core/dependencies.py +115 -0
- beru/core/deployment.py +45 -0
- beru/core/environment.py +223 -0
- beru/core/exceptions.py +638 -0
- beru/core/features.py +224 -0
- beru/core/host.py +212 -0
- beru/core/llm_catalog.py +404 -0
- beru/core/local_runner.py +144 -0
- beru/core/mcp_seed.py +215 -0
- beru/core/members.py +139 -0
- beru/core/model_registry.py +338 -0
- beru/core/public.py +107 -0
- beru/core/security.py +249 -0
- beru/core/uuid.py +18 -0
- beru/crud/__init__.py +0 -0
- beru/data/models_dev.json +9410 -0
- beru/king_beru.py +268 -0
- beru/main.py +544 -0
- beru/middleware/basic_auth.py +295 -0
- beru/middleware/error_handler.py +430 -0
- beru/models/__init__.py +23 -0
- beru/models/account_token.py +19 -0
- beru/models/agent.py +46 -0
- beru/models/agent_session.py +56 -0
- beru/models/app_session.py +41 -0
- beru/models/base.py +35 -0
- beru/models/chat_secret.py +21 -0
- beru/models/document.py +28 -0
- beru/models/email_outbox.py +23 -0
- beru/models/email_verification.py +28 -0
- beru/models/event_log.py +101 -0
- beru/models/execution.py +57 -0
- beru/models/execution_thread.py +39 -0
- beru/models/instance_setting.py +18 -0
- beru/models/llm_model.py +36 -0
- beru/models/mcp_server.py +121 -0
- beru/models/push_subscription.py +24 -0
- beru/models/schedule.py +30 -0
- beru/models/stripe_event.py +34 -0
- beru/models/user.py +84 -0
- beru/models/workflow.py +69 -0
- beru/models/workspace.py +84 -0
- beru/runner/__init__.py +18 -0
- beru/runner/__main__.py +5 -0
- beru/runner/config.py +103 -0
- beru/runner/core.py +1071 -0
- beru/runner/cron.py +138 -0
- beru/runner/engines.py +300 -0
- beru/runner/harness.py +139 -0
- beru/runner/mcp.py +254 -0
- beru/runner/procs.py +149 -0
- beru/runner/server.py +266 -0
- beru/schemas/__init__.py +64 -0
- beru/schemas/agent.py +115 -0
- beru/schemas/agent_session.py +147 -0
- beru/schemas/auth.py +87 -0
- beru/schemas/base.py +28 -0
- beru/schemas/billing.py +79 -0
- beru/schemas/document.py +75 -0
- beru/schemas/execution.py +126 -0
- beru/schemas/execution_thread.py +78 -0
- beru/schemas/llm_model.py +100 -0
- beru/schemas/mcp_server.py +134 -0
- beru/schemas/sync.py +78 -0
- beru/schemas/workflow.py +159 -0
- beru/services/__init__.py +7 -0
- beru/services/agent_run_tracker.py +159 -0
- beru/services/agent_runner.py +329 -0
- beru/services/agent_service.py +1107 -0
- beru/services/agent_session_service.py +145 -0
- beru/services/auth_service.py +370 -0
- beru/services/base.py +245 -0
- beru/services/beru_models.py +302 -0
- beru/services/beru_services.py +269 -0
- beru/services/billing_service.py +492 -0
- beru/services/chat_secrets.py +143 -0
- beru/services/compaction.py +264 -0
- beru/services/context_budget.py +133 -0
- beru/services/conversation_service.py +1031 -0
- beru/services/document_service.py +391 -0
- beru/services/duplication.py +123 -0
- beru/services/email_service.py +280 -0
- beru/services/error_reports.py +212 -0
- beru/services/event_log_service.py +249 -0
- beru/services/execution_engine.py +1010 -0
- beru/services/execution_service.py +897 -0
- beru/services/execution_thread_service.py +265 -0
- beru/services/execution_types.py +39 -0
- beru/services/knowledge_base.py +465 -0
- beru/services/llm_provider_service.py +412 -0
- beru/services/llm_service.py +273 -0
- beru/services/mcp_integration.py +605 -0
- beru/services/mcp_oauth.py +441 -0
- beru/services/mcp_server_service.py +1684 -0
- beru/services/monitor.py +399 -0
- beru/services/monitoring.py +676 -0
- beru/services/nodes/__init__.py +34 -0
- beru/services/nodes/agent_handler.py +582 -0
- beru/services/nodes/base.py +109 -0
- beru/services/nodes/condition_handler.py +267 -0
- beru/services/nodes/constant_handler.py +94 -0
- beru/services/nodes/control_handler.py +650 -0
- beru/services/nodes/external_agent_handler.py +385 -0
- beru/services/nodes/flow_handler.py +365 -0
- beru/services/nodes/tool_handler.py +612 -0
- beru/services/prompt_manager.py +411 -0
- beru/services/push.py +272 -0
- beru/services/rag_service.py +400 -0
- beru/services/run_registry.py +263 -0
- beru/services/run_user.py +43 -0
- beru/services/schedules.py +395 -0
- beru/services/session_turn.py +279 -0
- beru/services/state_manager.py +337 -0
- beru/services/sync_service.py +359 -0
- beru/services/task_runner.py +287 -0
- beru/services/titles.py +155 -0
- beru/services/tools/__init__.py +16 -0
- beru/services/tools/admin.py +480 -0
- beru/services/tools/ask.py +103 -0
- beru/services/tools/deferred.py +191 -0
- beru/services/tools/live_sessions.py +331 -0
- beru/services/tools/native.py +306 -0
- beru/services/tools/report.py +189 -0
- beru/services/tools/runner_tools.py +291 -0
- beru/services/tools/schedules.py +272 -0
- beru/services/tools/share.py +52 -0
- beru/services/tools/workspace.py +421 -0
- beru/services/uploads.py +356 -0
- beru/services/workflow_service.py +1293 -0
- beru/services/workflow_validator.py +547 -0
- beru/starters/__init__.py +576 -0
- beru/starters/agents/architect.json +13 -0
- beru/starters/agents/author.json +13 -0
- beru/starters/agents/closer.json +13 -0
- beru/starters/agents/copywriter.json +13 -0
- beru/starters/agents/dealdesk.json +13 -0
- beru/starters/agents/developer.json +10 -0
- beru/starters/agents/editor.json +16 -0
- beru/starters/agents/generalist.json +10 -0
- beru/starters/agents/integrator.json +13 -0
- beru/starters/agents/judge.json +13 -0
- beru/starters/agents/marketing-editor.json +13 -0
- beru/starters/agents/planner.json +13 -0
- beru/starters/agents/qa-reviewer.json +17 -0
- beru/starters/agents/researcher.json +13 -0
- beru/starters/agents/reviewer-clarity.json +13 -0
- beru/starters/agents/reviewer-correctness.json +13 -0
- beru/starters/agents/reviewer-risk.json +13 -0
- beru/starters/agents/reviewer.json +16 -0
- beru/starters/agents/scout.json +13 -0
- beru/starters/agents/spec-writer.json +13 -0
- beru/starters/agents/strategist.json +13 -0
- beru/starters/agents/tech-lead.json +16 -0
- beru/starters/workflows/bizdev-team.json +192 -0
- beru/starters/workflows/dev-team.json +152 -0
- beru/starters/workflows/marketing-team.json +152 -0
- beru/starters/workflows/plan-delegate-integrate.json +119 -0
- beru/starters/workflows/react-baseline.json +53 -0
- beru/starters/workflows/review-panel.json +134 -0
- beru/starters/workflows/reviewed-answer.json +131 -0
- beru/starters/workflows/second-opinion.json +105 -0
- beru/starters/workflows/workspace-handoff.json +95 -0
- beru/static/ui/android-chrome-192x192.png +0 -0
- beru/static/ui/android-chrome-512x512.png +0 -0
- beru/static/ui/apple-touch-icon.png +0 -0
- beru/static/ui/assets/html2canvas.esm-CBrSDip1.js +22 -0
- beru/static/ui/assets/index-8V-nI-TB.js +918 -0
- beru/static/ui/assets/index-DgIeHdmD.css +1 -0
- beru/static/ui/assets/index.es-D2jYrrlu.js +18 -0
- beru/static/ui/assets/jspdf.es.min-D32WELE7.js +170 -0
- beru/static/ui/assets/jspdf.plugin.autotable-DYHf0IZ0.js +2 -0
- beru/static/ui/assets/pdf-BnM3EHEC.js +4 -0
- beru/static/ui/assets/purify.es-CAi2OJFm.js +3 -0
- beru/static/ui/assets/server.browser-BtnbRC1f.js +27 -0
- beru/static/ui/assets/svg2pdf.es.min-DANJbn6A.js +2 -0
- beru/static/ui/beru-crest.png +0 -0
- beru/static/ui/favicon-16x16.png +0 -0
- beru/static/ui/favicon-32x32.png +0 -0
- beru/static/ui/favicon.ico +0 -0
- beru/static/ui/index.html +50 -0
- beru/static/ui/maskable-icon-512x512.png +0 -0
- beru/static/ui/placeholder.svg +1 -0
- beru/static/ui/robots.txt +14 -0
- beru/static/ui/site.webmanifest +74 -0
- beru/static/ui/splash/apple-splash-1125x2436.png +0 -0
- beru/static/ui/splash/apple-splash-1170x2532.png +0 -0
- beru/static/ui/splash/apple-splash-1179x2556.png +0 -0
- beru/static/ui/splash/apple-splash-1206x2622.png +0 -0
- beru/static/ui/splash/apple-splash-1242x2688.png +0 -0
- beru/static/ui/splash/apple-splash-1284x2778.png +0 -0
- beru/static/ui/splash/apple-splash-1290x2796.png +0 -0
- beru/static/ui/splash/apple-splash-1320x2868.png +0 -0
- beru/static/ui/splash/apple-splash-1536x2048.png +0 -0
- beru/static/ui/splash/apple-splash-1640x2360.png +0 -0
- beru/static/ui/splash/apple-splash-1668x2388.png +0 -0
- beru/static/ui/splash/apple-splash-2048x2732.png +0 -0
- beru/static/ui/splash/apple-splash-750x1334.png +0 -0
- beru/static/ui/splash/apple-splash-828x1792.png +0 -0
- beru/static/ui/sw.js +114 -0
- beru/utils/__init__.py +0 -0
- beru/utils/embedding_utils.py +247 -0
- beru/utils/llm_clients.py +584 -0
- beru/utils/serialization.py +86 -0
- beru/utils/text_processing.py +428 -0
- beru-1.0.0a1.dist-info/METADATA +91 -0
- beru-1.0.0a1.dist-info/RECORD +254 -0
- beru-1.0.0a1.dist-info/WHEEL +5 -0
- beru-1.0.0a1.dist-info/entry_points.txt +3 -0
- beru-1.0.0a1.dist-info/licenses/LICENSE +21 -0
- 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
|
beru/api/v1/__init__.py
ADDED
|
@@ -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
|
+
|