Skip to content
Draft
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
1 change: 1 addition & 0 deletions agents/chat/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ARG UV_VERSION=0.10.7
FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv
FROM python:3.14-alpine3.23@sha256:faee120f7885a06fcc9677922331391fa690d911c020abb9e8025ff3d908e510
ARG RELEASE_VERSION="main"
RUN apk add bash git nodejs npm curl
COPY ./agents/chat/ /app/agents/chat
COPY ./apps/agentstack-sdk-py/ /app/apps/agentstack-sdk-py/
WORKDIR /app/agents/chat
Expand Down
10 changes: 5 additions & 5 deletions agents/chat/src/chat/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
LLMServiceExtensionSpec,
TrajectoryExtensionServer,
TrajectoryExtensionSpec,
)
from agentstack_sdk.a2a.extensions.services.platform import (
PlatformApiExtensionServer,
PlatformApiExtensionSpec,
)
Expand Down Expand Up @@ -259,15 +257,17 @@ async def chat(
input=last_step.input, output=last_step.output, error=last_step.error
)
metadata = trajectory.trajectory_metadata(
title=last_step.tool.name if last_step.tool else None, content=trajectory_content.model_dump_json(), group_id=last_step.id
title=last_step.tool.name if last_step.tool else None,
content=trajectory_content.model_dump_json(),
group_id=last_step.id,
)
yield metadata
await context.store(AgentMessage(metadata=metadata))

if isinstance(last_step.output, FileCreatorToolOutput):
for file_info in last_step.output.result.files:
part = file_info.file.to_file_part()
part.file.name = file_info.display_filename
part = file_info.file.to_part()
part.filename = file_info.display_filename
artifact = AgentArtifact(name=file_info.display_filename, parts=[part])
yield artifact
await context.store(artifact)
Expand Down
6 changes: 3 additions & 3 deletions agents/chat/src/chat/tools/files/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ class OriginType(StrEnum):


ORIGIN_TYPE_BY_ROLE = {
Role.user: OriginType.UPLOADED,
Role.agent: OriginType.GENERATED,
Role.ROLE_USER: OriginType.UPLOADED,
Role.ROLE_AGENT: OriginType.GENERATED,
}


class FileChatInfo(BaseModel):
class FileChatInfo(BaseModel, arbitrary_types_allowed=True):
file: File
display_filename: str # A sanitized version of the filename used for display, in case of naming conflicts.
role: Role
Expand Down
21 changes: 10 additions & 11 deletions agents/chat/src/chat/tools/files/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Iterable

import pydantic
from a2a.types import FilePart, FileWithUri, Message, Role
from a2a.types import Message, Role
from beeai_framework.backend import AnyMessage, AssistantMessage, UserMessage

from agentstack_sdk.platform import File
Expand All @@ -17,14 +17,14 @@


def to_framework_message(message: Message, all_attachments: list[FileChatInfo]) -> AnyMessage:
message_text = "".join(part.root.text for part in message.parts if part.root.kind == "text")
message_text = "".join(part.text for part in message.parts if part.HasField("text"))
if attachments := [file for file in all_attachments if file.message_id == message.message_id]:
message_text += "\nAttached files:\n" + "\n".join([file.description for file in attachments])

match message.role:
case Role.agent:
case Role.ROLE_AGENT:
return AssistantMessage(message_text)
case Role.user:
case Role.ROLE_USER:
return UserMessage(message_text)
case _:
raise ValueError(f"Invalid message role: {message.role}")
Expand All @@ -46,13 +46,12 @@ async def extract_files(history: list[Message]) -> list[FileChatInfo]:

for item in history:
for part in item.parts:
match part.root:
case FilePart(file=FileWithUri(uri=uri)):
with suppress(ValueError):
url = pydantic.type_adapter.TypeAdapter(PlatformFileUrl).validate_python(uri)
if url.file_id not in seen:
seen.add(url.file_id)
files[url.file_id] = item
if part.HasField("url"):
with suppress(ValueError):
url = pydantic.type_adapter.TypeAdapter(PlatformFileUrl).validate_python(part.url)
if url.file_id not in seen:
seen.add(url.file_id)
files[url.file_id] = item

# TODO: N+1 query issue, add bulk endpoint
file_objects = await asyncio.gather(*(File.get(file_id) for file_id in files))
Expand Down
657 changes: 280 additions & 377 deletions agents/chat/uv.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ async def content_builder_agent(

updated_files = await agent_stack_backend.alist(order_by="created_at", order="asc", created_after=started_at)
for updated_file in updated_files:
yield updated_file.to_file_part()
yield updated_file.to_part()


def serve():
Expand Down
2 changes: 1 addition & 1 deletion agents/rag/src/rag/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ async def handle_tool_success(event, meta):
if isinstance(event.output, FileCreatorToolOutput):
result = event.output.result
for file in result.files:
artifact = AgentArtifact(name=file.filename, parts=[file.to_file_part()])
artifact = AgentArtifact(name=file.filename, parts=[file.to_part()])
await context.yield_async(artifact)

response = (
Expand Down
20 changes: 20 additions & 0 deletions apps/agentstack-cli/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "agentstack cli",
"type": "debugpy",
"env": {
"AGENTSTACK__DEBUG": "true"
},
"request": "launch",
"module": "agentstack_cli",
"justMyCode": false,
"console": "integratedTerminal",
"args": "${command:pickArgs}"
}
]
}
6 changes: 6 additions & 0 deletions apps/agentstack-cli/src/agentstack_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

from agentstack_cli import app

app()
9 changes: 6 additions & 3 deletions apps/agentstack-cli/src/agentstack_cli/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
import httpx
import openai
import pydantic
from a2a.client import A2AClientHTTPError, Client, ClientConfig, ClientFactory
from a2a.client import A2AClientError, Client, ClientConfig, ClientFactory
from a2a.types import AgentCard
from agentstack_sdk.platform.context import ContextToken
from google.protobuf.json_format import MessageToDict
from httpx import HTTPStatusError
from httpx._types import RequestFiles

from agentstack_cli import configuration
from agentstack_cli.configuration import Configuration
from agentstack_cli.utils import pick

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -140,9 +142,10 @@ async def a2a_client(agent_card: AgentCard, context_token: ContextToken) -> Asyn
yield ClientFactory(ClientConfig(httpx_client=httpx_client, use_client_preference=True)).create(
card=agent_card
)
except A2AClientHTTPError as ex:
except A2AClientError as ex:
card_data = json.dumps(
agent_card.model_dump(include={"url", "additional_interfaces", "preferred_transport"}), indent=2
pick(MessageToDict(agent_card), {"url", "additional_interfaces", "preferred_transport"}),
indent=2,
)
raise RuntimeError(
f"The agent is not reachable, please check that the agent card is configured properly.\n"
Expand Down
Loading
Loading