mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[Haik]: ckpt prompt building done (both the recursive system prompt and the actual prompt construction when a user adds context paths, forced tools, or skills)
This commit is contained in:
@@ -34,6 +34,7 @@ from backend.apps.agents.session_store import (
|
||||
load,
|
||||
)
|
||||
from backend.apps.agents import ws
|
||||
from backend.apps.agents.compose_system_prompt import compose_system_prompt
|
||||
from claude_agent_sdk import ClaudeAgentOptions
|
||||
|
||||
SESSIONS: dict[str, Agent] = {}
|
||||
@@ -51,7 +52,7 @@ async def p_send_browser_command(
|
||||
action: str, browser_id: str, tab_id: str, params: dict,
|
||||
) -> dict:
|
||||
"""BrowserCommandFn implementation that routes through the browser FutureBridge."""
|
||||
request_id = uuid4().hex
|
||||
request_id: str = uuid4().hex
|
||||
if not ws.has_global_connections():
|
||||
return {"error": "No dashboard connected. Open the dashboard to use browser tools."}
|
||||
return await ws.BROWSER_BRIDGE.request(
|
||||
@@ -133,20 +134,22 @@ class LaunchBody(BaseModel):
|
||||
|
||||
@agents.router.post("/launch")
|
||||
async def launch(body: LaunchBody) -> dict:
|
||||
# TODO: build ClaudeAgentOptions from body once prompt/options builder exists
|
||||
system_prompt = compose_system_prompt(
|
||||
session_prompt=body.system_prompt or None,
|
||||
)
|
||||
agent: Agent = Agent(
|
||||
model=body.model,
|
||||
mode=body.mode,
|
||||
status="running",
|
||||
status="stopped",
|
||||
config=ClaudeAgentOptions(
|
||||
system_prompt=body.system_prompt,
|
||||
system_prompt=system_prompt,
|
||||
max_turns=body.max_turns,
|
||||
),
|
||||
)
|
||||
agent.on_event = p_make_session_emitter(agent.session_id)
|
||||
SESSIONS[agent.session_id] = agent
|
||||
await agent._emit(AgentStatusEvent(
|
||||
session_id=agent.session_id, status="running",
|
||||
await agent.emit(AgentStatusEvent(
|
||||
session_id=agent.session_id, status="stopped",
|
||||
session=agent.snapshot(),
|
||||
))
|
||||
return {"session_id": agent.session_id, "session": agent.snapshot().model_dump(mode="json")}
|
||||
@@ -163,7 +166,7 @@ async def update_session(session_id: str, body: UpdateBody) -> dict:
|
||||
agent.name = body.name
|
||||
if body.system_prompt is not None:
|
||||
agent.config.system_prompt = body.system_prompt
|
||||
await agent._emit(AgentStatusEvent(
|
||||
await agent.emit(AgentStatusEvent(
|
||||
session_id=session_id, status=agent.status,
|
||||
session=agent.snapshot(),
|
||||
))
|
||||
@@ -254,7 +257,7 @@ async def edit_message(session_id: str, body: EditMessageBody) -> dict:
|
||||
fork: Agent = agent.branch(body.message_id)
|
||||
SESSIONS[fork.session_id] = fork
|
||||
|
||||
edited_msg = UserMessage(content=body.content, branch_id=fork.branch_id)
|
||||
edited_msg: UserMessage = UserMessage(content=body.content, branch_id=fork.branch_id)
|
||||
await fork.send_message(edited_msg)
|
||||
return {"ok": True, "branch_id": fork.branch_id, "session_id": fork.session_id}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Pure prompt-building helpers.
|
||||
|
||||
All functions are stateless — they accept data as parameters and return
|
||||
strings. No imports from ``apps/`` or any external stores.
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
from typeguard import typechecked
|
||||
|
||||
@typechecked
|
||||
def compose_system_prompt(
|
||||
global_default: Optional[str] = None,
|
||||
mode_prompt: Optional[str] = None,
|
||||
session_prompt: Optional[str] = None,
|
||||
connected_tools_ctx: Optional[str] = None,
|
||||
browser_ctx: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""Layer multiple prompt sources into one system prompt.
|
||||
|
||||
Order matters — earlier layers provide base context, later layers
|
||||
override or augment.
|
||||
"""
|
||||
parts: List[str] = [p for p in (
|
||||
global_default,
|
||||
mode_prompt,
|
||||
session_prompt,
|
||||
connected_tools_ctx,
|
||||
browser_ctx,
|
||||
) if p]
|
||||
return "\n\n".join(parts) if parts else None
|
||||
@@ -12,6 +12,9 @@ from backend.core.shared_structs.agent.Message.agent_inputs import (
|
||||
from backend.core.shared_structs.agent.Message.agent_outputs import (
|
||||
ToolCallContent, ToolResultContent
|
||||
)
|
||||
from backend.core.shared_structs.agent.Message.prompt_utils import (
|
||||
resolve_context_paths, resolve_forced_tools, resolve_attached_skills,
|
||||
)
|
||||
|
||||
class Message(BaseModel):
|
||||
id: str = Field(default_factory=lambda: uuid4().hex)
|
||||
@@ -39,7 +42,15 @@ class UserMessage(Message):
|
||||
|
||||
@typechecked
|
||||
def to_prompt(self) -> PromptMsgDict:
|
||||
blocks: List[PromptBlock] = [TextPromptBlock(type="text", text=self.content)]
|
||||
parts: List[str] = [
|
||||
resolve_forced_tools(self.forced_tools),
|
||||
resolve_context_paths(self.context_paths),
|
||||
resolve_attached_skills(self.attached_skills),
|
||||
self.content,
|
||||
]
|
||||
full_text: str = "\n\n".join(p for p in parts if p)
|
||||
|
||||
blocks: List[PromptBlock] = [TextPromptBlock(type="text", text=full_text)]
|
||||
for data, media_type in zip[tuple[str, str]](self.images, self.image_media_types):
|
||||
blocks.append(ImagePromptBlock(
|
||||
type="image",
|
||||
|
||||
@@ -30,4 +30,5 @@ class ContextPath(BaseModel):
|
||||
|
||||
class SkillMeta(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
name: str
|
||||
content: str = ""
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Pure prompt-building helpers.
|
||||
|
||||
All functions are stateless — they accept data as parameters and return
|
||||
strings. No imports from ``apps/`` or any external stores.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from backend.core.shared_structs.agent.Message.agent_inputs import ContextPath, SkillMeta
|
||||
from typeguard import typechecked
|
||||
|
||||
MAX_FILE_READ_BYTES = 512_000
|
||||
DIR_TREE_MAX_DEPTH = 4
|
||||
|
||||
@typechecked
|
||||
def p_build_dir_tree(
|
||||
root: str,
|
||||
max_depth: int = DIR_TREE_MAX_DEPTH,
|
||||
prefix: str = "",
|
||||
) -> List[str]:
|
||||
lines: List[str] = []
|
||||
try:
|
||||
entries: List[str] = sorted(os.listdir(root))
|
||||
except PermissionError:
|
||||
return [f"{prefix}[permission denied]"]
|
||||
dirs: List[str] = [
|
||||
e for e in entries
|
||||
if not e.startswith(".") and os.path.isdir(os.path.join(root, e))
|
||||
]
|
||||
files: List[str] = [
|
||||
e for e in entries
|
||||
if not e.startswith(".") and os.path.isfile(os.path.join(root, e))
|
||||
]
|
||||
for f in files:
|
||||
lines.append(f"{prefix}{f}")
|
||||
for d in dirs:
|
||||
lines.append(f"{prefix}{d}/")
|
||||
if max_depth > 1:
|
||||
lines.extend(p_build_dir_tree(os.path.join(root, d), max_depth - 1, prefix + " "))
|
||||
return lines
|
||||
|
||||
|
||||
@typechecked
|
||||
def resolve_context_paths(context_paths: List[ContextPath]) -> str:
|
||||
"""Read files / build directory trees for each context path."""
|
||||
if not context_paths:
|
||||
return ""
|
||||
sections: List[str] = []
|
||||
for cp in context_paths:
|
||||
path: str = cp.path
|
||||
cp_type: str = cp.type
|
||||
if not path or not os.path.exists(path):
|
||||
sections.append(f"[Context: {path} — not found]")
|
||||
continue
|
||||
if cp_type == "file" and os.path.isfile(path):
|
||||
try:
|
||||
with open(path, "r", errors="replace") as f:
|
||||
content: str = f.read(MAX_FILE_READ_BYTES)
|
||||
sections.append(
|
||||
f'<context_file path="{path}">\n{content}\n</context_file>'
|
||||
)
|
||||
except Exception as e:
|
||||
sections.append(f"[Context: {path} — error reading: {e}]")
|
||||
elif cp_type == "directory" and os.path.isdir(path):
|
||||
tree = p_build_dir_tree(path, max_depth=DIR_TREE_MAX_DEPTH)
|
||||
sections.append(
|
||||
f'<context_directory path="{path}">\n{chr(10).join(tree)}\n</context_directory>'
|
||||
)
|
||||
else:
|
||||
sections.append(f"[Context: {path} — type mismatch]")
|
||||
return "\n\n".join(sections)
|
||||
|
||||
|
||||
@typechecked
|
||||
def resolve_forced_tools(forced_tools: List[str]) -> str:
|
||||
"""Build a <forced_tools> prompt block from a list of tool names."""
|
||||
if not forced_tools:
|
||||
return ""
|
||||
lines: List[str] = [f"- {name}" for name in forced_tools]
|
||||
return (
|
||||
"<forced_tools>\n"
|
||||
"The user explicitly requested these tools be used. "
|
||||
"Prioritize using them to address the user's request.\n"
|
||||
+ "\n".join(lines)
|
||||
+ "\n</forced_tools>"
|
||||
)
|
||||
|
||||
|
||||
@typechecked
|
||||
def resolve_attached_skills(attached_skills: List[SkillMeta]) -> str:
|
||||
"""Format attached skills into prompt sections."""
|
||||
if not attached_skills:
|
||||
return ""
|
||||
sections: List[str] = []
|
||||
for skill in attached_skills:
|
||||
name: str = skill.name
|
||||
content: str = skill.content
|
||||
if content:
|
||||
sections.append(f"[Using skill: {name}]\n\n{content}")
|
||||
return "\n\n".join(sections)
|
||||
Reference in New Issue
Block a user