Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions flocks/input/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from flocks.command.command import CommandInfo
from flocks.input.types import InputSourceType, surface_for_source
from flocks.session.execution_mode import SessionExecutionMode


class UserInputEvent(BaseModel):
Expand All @@ -31,6 +32,10 @@ class UserInputEvent(BaseModel):
mock_reply: Optional[str] = Field(None, alias="mockReply")
system: Optional[str] = None
tools: Optional[Dict[str, bool]] = None
execution_mode: SessionExecutionMode = Field(
SessionExecutionMode.BUILD,
alias="executionMode",
)

@property
def surface(self) -> str:
Expand Down
111 changes: 95 additions & 16 deletions flocks/server/routes/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
is_model_auto_session_category,
)
from flocks.session.policy import SessionPolicy
from flocks.session.execution_mode import SessionExecutionMode
from flocks.utils.log import Log
from flocks.utils.json_repair import parse_json_robust, repair_truncated_json
from flocks.utils.monitor import get_monitor
Expand Down Expand Up @@ -1677,6 +1678,11 @@ class PromptRequest(BaseModel):
tools: Optional[Dict[str, bool]] = Field(None, description="Tool settings (deprecated)")
system: Optional[str] = Field(None, description="System prompt override")
variant: Optional[str] = Field(None, description="Model variant")
execution_mode: SessionExecutionMode = Field(
SessionExecutionMode.BUILD,
alias="executionMode",
description="Execution mode for this user turn",
)


class UserMessageInfo(BaseModel):
Expand Down Expand Up @@ -1705,6 +1711,7 @@ class UserMessageInfo(BaseModel):
tools: Optional[Dict[str, bool]] = None
variant: Optional[str] = None
compacted: Optional[bool] = None
executionMode: SessionExecutionMode = SessionExecutionMode.BUILD


class AssistantMessageInfo(BaseModel):
Expand Down Expand Up @@ -1861,6 +1868,11 @@ async def _message_to_response_info(msg: Any, *, cwd: str) -> MessageInfo:
agent=getattr(msg, "agent", None) or DEFAULT_AGENT,
model=model_info,
compacted=getattr(msg, "compacted", None),
executionMode=getattr(
msg,
"executionMode",
SessionExecutionMode.BUILD,
),
)

tokens_raw = getattr(msg, "tokens", None)
Expand Down Expand Up @@ -2628,6 +2640,25 @@ async def send_session_message(sessionID: str, request: PromptRequest, http_requ
_require_session_write_access(session, current_user)

working_directory = await _resolve_session_working_directory(session)
_validate_execution_mode_request(request)
if request.execution_mode == SessionExecutionMode.GOAL:
from flocks.session.goal import GoalManager

objective = _extract_text_from_parts(request.parts).strip()
state = await GoalManager.set_goal(sessionID, objective)
await publish_event("session.goal.updated", {
"sessionID": sessionID,
"status": state.status,
"objective": state.objective,
"reason": state.last_reason,
})
request = request.model_copy(update={
"parts": _replace_text_parts(
request.parts,
GoalManager.goal_prompt(state.objective),
),
"display_text": request.display_text or objective,
})

log.info("session.message.send.processing", {
"sessionID": sessionID,
Expand Down Expand Up @@ -3077,6 +3108,7 @@ async def _process_session_message(
time={"created": now_ms},
agent=agent_name,
model={"providerID": provider_id, "modelID": model_id},
executionMode=request.execution_mode,
part_id=user_part_id,
part_metadata=display_metadata,
synthetic=True if _is_no_reply else None,
Expand All @@ -3091,6 +3123,7 @@ async def _process_session_message(
"time": {"created": now_ms},
"agent": agent_name,
"model": {"providerID": provider_id, "modelID": model_id},
"executionMode": request.execution_mode.value,
}
})
_part_event: dict = {
Expand Down Expand Up @@ -3508,6 +3541,32 @@ def _extract_text_from_parts(parts: List[Dict[str, Any]]) -> str:
return "".join(part.get("text", "") for part in parts if part.get("type") == "text")


def _validate_execution_mode_request(request: PromptRequest) -> None:
if request.execution_mode != SessionExecutionMode.GOAL:
return
objective = _extract_text_from_parts(request.parts).strip()
if not objective:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Goal mode requires a non-empty text objective",
)
if any(part.get("type") != "text" for part in request.parts):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Goal mode does not support attachments",
)


def _event_text_for_execution_mode(
parts: List[Dict[str, Any]],
execution_mode: SessionExecutionMode,
) -> str:
text = _extract_text_from_parts(parts)
if execution_mode == SessionExecutionMode.GOAL:
return f"/goal {text.strip()}"
return text


def _replace_text_parts(
parts: Optional[List[Dict[str, Any]]],
text: str,
Expand Down Expand Up @@ -3613,7 +3672,7 @@ def _event_from_queued_prompt(item, working_directory: str):
return UserInputEvent(
source_type="webui",
sessionID=item.sessionID,
text=_extract_text_from_parts(item.parts),
text=_event_text_for_execution_mode(item.parts, item.executionMode),
parts=[dict(part) for part in item.parts],
agent=item.agent,
model=item.model,
Expand All @@ -3624,6 +3683,7 @@ def _event_from_queued_prompt(item, working_directory: str):
mockReply=item.mockReply,
tools=item.tools,
system=item.system,
executionMode=item.executionMode,
working_directory=working_directory,
)

Expand Down Expand Up @@ -3760,13 +3820,14 @@ def _build_prompt_request_from_event(event, prompt_text: str, display_text: Opti
noReply=event.no_reply,
tools=event.tools,
system=event.system,
execution_mode=event.execution_mode,
)


async def _dispatch_sse_input(sessionID: str, session, event, working_directory: str) -> None:
import time as _time

from flocks.input.dispatcher import dispatch_user_input
from flocks.input.dispatcher import dispatch_user_input, parse_slash_command
from flocks.input.output import SSEOutputSink
from flocks.server.routes.event import publish_event
from flocks.session.message import Message, MessageRole
Expand All @@ -3791,6 +3852,7 @@ async def _create_user_message(
id=user_msg_id,
time={"created": now_ms},
agent=message_agent,
executionMode=event.execution_mode,
**({"model": model_info} if model_info else {}),
part_id=user_part_id,
)
Expand All @@ -3801,6 +3863,7 @@ async def _create_user_message(
"role": "user",
"time": {"created": now_ms},
"agent": message_agent,
"executionMode": event.execution_mode.value,
**({"model": model_info} if model_info else {}),
}
})
Expand Down Expand Up @@ -3874,6 +3937,18 @@ async def _publish_direct_response(output_event, text: str) -> None:
)

async def _run_llm(output_event, prompt_text: str, display_text: Optional[str] = None) -> None:
parsed = parse_slash_command(output_event.text, output_event.metadata)
if parsed is not None and parsed.canonical_name == "goal":
from flocks.session.goal import GoalManager

state = await GoalManager.get(sessionID)
if state is not None:
await publish_event("session.goal.updated", {
"sessionID": sessionID,
"status": state.status,
"objective": state.objective,
"reason": state.last_reason,
})
request = _build_prompt_request_from_event(output_event, prompt_text, display_text)
await _process_session_message(sessionID, session, request, working_directory)

Expand Down Expand Up @@ -3918,18 +3993,7 @@ async def _run_session_control(output_event, parsed) -> bool:
session_control=_run_session_control,
clear_history=_clear_history,
)
result = await dispatch_user_input(event, sink)
if result.command_name == "goal" and result.action == "llm":
from flocks.session.goal import GoalManager

state = await GoalManager.get(sessionID)
if state is not None:
await publish_event("session.goal.updated", {
"sessionID": sessionID,
"status": state.status,
"objective": state.objective,
"reason": state.last_reason,
})
await dispatch_user_input(event, sink)


class PromptQueueUpdateRequest(BaseModel):
Expand All @@ -3942,6 +4006,7 @@ async def _enqueue_prompt_request(
):
from flocks.session.interaction_queue import InteractionQueue

_validate_execution_mode_request(request)
await _require_agent_usable_for_chat(request.agent)
model = request.model.model_dump(by_alias=True) if request.model else None
parts = _materialize_queued_parts(session_id, [dict(part) for part in request.parts])
Expand All @@ -3957,6 +4022,7 @@ async def _enqueue_prompt_request(
mock_reply=request.mockReply,
tools=request.tools,
system=request.system,
execution_mode=request.execution_mode,
)


Expand Down Expand Up @@ -4105,27 +4171,40 @@ async def send_session_message_async(
_require_session_write_access(session, current_user)

working_directory = await _resolve_session_working_directory(session)
_validate_execution_mode_request(request)
await _require_agent_usable_for_chat(request.agent)

log.info("session.prompt_async.accepted", {
"sessionID": sessionID,
"directory": working_directory,
})

event_text = _event_text_for_execution_mode(
request.parts,
request.execution_mode,
)
event_display_text = request.display_text
if request.execution_mode == SessionExecutionMode.GOAL:
event_display_text = (
event_display_text
or _extract_text_from_parts(request.parts).strip()
)

event = UserInputEvent(
source_type="webui",
sessionID=sessionID,
text=_extract_text_from_parts(request.parts),
text=event_text,
parts=[dict(part) for part in request.parts],
agent=request.agent,
model=request.model.model_dump(by_alias=True) if request.model else None,
variant=request.variant,
display_text=request.display_text,
display_text=event_display_text,
messageID=request.messageID,
noReply=request.noReply,
mockReply=request.mockReply,
tools=request.tools,
system=request.system,
executionMode=request.execution_mode,
working_directory=working_directory,
)

Expand Down
Loading