Files
Stirling-PDF/engine/src/stirling/agents/user_spec.py
T
Anthony Stirling 798ba57f0b Reply to chat in the user's UI language (#7766)
# Description of Changes

Pass a user browser lang ID to engine

<img width="1400" height="900" alt="image"
src="https://github.com/user-attachments/assets/7e8fc5c2-8881-4a74-b718-7f5cd350d457"
/>



---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] Every comment I added says something the code does not
([guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/CODE_COMMENTS.md))
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-09-02 12:35:22 +00:00

124 lines
5.1 KiB
Python

from __future__ import annotations
from pydantic_ai import Agent
from pydantic_ai.output import NativeOutput
from stirling.agents.pdf_edit import PdfEditAgent
from stirling.contracts import (
AgentDraft,
AgentDraftRequest,
AgentDraftResponse,
AgentDraftWorkflowResponse,
AgentRevisionRequest,
AgentRevisionResponse,
AgentRevisionWorkflowResponse,
AiToolAgentStep,
ConversationMessage,
EditPlanResponse,
OrchestratorRequest,
PdfEditRequest,
PdfEditTerminalResponse,
format_conversation_history,
)
from stirling.models import ApiModel
from stirling.services import AppRuntime, language_directive
class UserSpecMetadata(ApiModel):
name: str
description: str
objective: str
class UserSpecAgent:
def __init__(self, runtime: AppRuntime) -> None:
self.runtime = runtime
self.pdf_edit_agent = PdfEditAgent(runtime)
self.agent = Agent(
model=runtime.smart_model,
output_type=NativeOutput(UserSpecMetadata),
system_prompt=(
"Create or revise a saved agent draft from the provided request and edit plan. "
"Return a concise name, description, and objective. "
"Keep the workflow grounded and practical."
),
model_settings=runtime.smart_model_settings,
)
async def orchestrate(self, request: OrchestratorRequest) -> AgentDraftWorkflowResponse:
"""Entry point for the orchestrator delegate — adapts the orchestrator's
request shape into an :class:`AgentDraftRequest` and runs the standard
:meth:`draft` pipeline.
"""
return await self.draft(
AgentDraftRequest(
user_message=request.user_message,
conversation_history=request.conversation_history,
)
)
async def draft(self, request: AgentDraftRequest) -> AgentDraftWorkflowResponse:
edit_plan = await self._build_edit_plan(request.user_message, request.conversation_history)
if not isinstance(edit_plan, EditPlanResponse):
return edit_plan
return AgentDraftResponse(draft=await self._run_draft_agent(request, edit_plan))
async def revise(self, request: AgentRevisionRequest) -> AgentRevisionWorkflowResponse:
edit_plan = await self._build_edit_plan(
f"Current objective: {request.current_draft.objective}\nRevision request: {request.user_message}",
request.conversation_history,
)
if not isinstance(edit_plan, EditPlanResponse):
return edit_plan
return AgentRevisionResponse(draft=await self._run_revision_agent(request, edit_plan))
async def _run_draft_agent(self, request: AgentDraftRequest, edit_plan: EditPlanResponse) -> AgentDraft:
metadata = (await self.agent.run(self._build_draft_prompt(request, edit_plan))).output
return AgentDraft(
name=metadata.name,
description=metadata.description,
objective=metadata.objective,
steps=[*edit_plan.steps],
)
async def _run_revision_agent(self, request: AgentRevisionRequest, edit_plan: EditPlanResponse) -> AgentDraft:
metadata = (await self.agent.run(self._build_revision_prompt(request, edit_plan))).output
preserved_ai_steps = [step for step in request.current_draft.steps if isinstance(step, AiToolAgentStep)]
return AgentDraft(
name=metadata.name,
description=metadata.description,
objective=metadata.objective,
steps=[*edit_plan.steps, *preserved_ai_steps],
)
def _build_draft_prompt(self, request: AgentDraftRequest, edit_plan: EditPlanResponse) -> str:
return (
f"User request:\n{request.user_message}\n\n"
f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n\n"
f"Edit plan summary:\n{edit_plan.summary}\n\n"
f"Edit plan rationale:\n{edit_plan.rationale or 'None'}\n\n"
f"Edit plan steps:\n{edit_plan.model_dump_json(indent=2)}"
f"\n\n{language_directive()}"
)
def _build_revision_prompt(self, request: AgentRevisionRequest, edit_plan: EditPlanResponse) -> str:
return (
f"Revision request:\n{request.user_message}\n\n"
f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n\n"
f"Current draft:\n{request.current_draft.model_dump_json(indent=2)}\n\n"
f"Edit plan summary:\n{edit_plan.summary}\n\n"
f"Edit plan rationale:\n{edit_plan.rationale or 'None'}\n\n"
f"Edit plan steps:\n{edit_plan.model_dump_json(indent=2)}"
f"\n\n{language_directive()}"
)
async def _build_edit_plan(
self,
user_message: str,
conversation_history: list[ConversationMessage],
) -> PdfEditTerminalResponse:
return await self.pdf_edit_agent.handle(
PdfEditRequest(user_message=user_message, conversation_history=conversation_history),
allow_need_content=False,
)