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 apps/agentstack-server/src/agentstack_server/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ class GenerateConversationTitleConfiguration(BaseModel):

CONVERSATION CONTENT:
```
{% if titleHint %}
Title hint:
{{ titleHint }}
{% endif %}

User message:
{{ text }}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ async def update_title(
self, *, context_id: UUID, title: str | None = None, generation_state: TitleGenerationState
) -> None:
# validate length before saving to database
_ = TypeAdapter(Metadata).validate_python({"title": title})
if title:
_ = TypeAdapter(Metadata).validate_python({"title": title})
context = await self.get(context_id=context_id)
query = (
contexts_table.update()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from datetime import timedelta
from uuid import UUID

from a2a.types import Artifact, FilePart, FileWithBytes, FileWithUri, Message, Role, TextPart
from a2a.types import Artifact, DataPart, FilePart, FileWithBytes, FileWithUri, Message, Role, TextPart
from fastapi import status
from kink import inject
from pydantic import TypeAdapter
Expand Down Expand Up @@ -155,10 +155,23 @@ async def update_last_active(self, *, context_id: UUID) -> None:
await uow.contexts.update_last_active(context_id=context_id)
await uow.commit()

def _extract_content_for_title(self, msg: Message | Artifact) -> tuple[str, Sequence[FileWithUri | FileWithBytes]]:
text_parts = [part.root.text for part in msg.parts if isinstance(part.root, TextPart)]
files = [part.root.file for part in msg.parts if isinstance(part.root, FilePart)]
return "".join(text_parts), files
def _extract_content_for_title(
self, msg: Message | Artifact
) -> tuple[str, str | None, Sequence[FileWithUri | FileWithBytes]]:
title_hint: str | None = None
text_parts: list[str] = []
files: list[FileWithUri | FileWithBytes] = []
for part in msg.parts:
match part.root:
case TextPart(text=text):
text_parts.append(text)
case DataPart(data={"title_hint": str(hint)}) if hint and not title_hint:
title_hint = hint
case FilePart(file=file):
files.append(file)
case _:
pass
return "".join(text_parts), title_hint, files

async def add_history_item(self, *, context_id: UUID, data: ContextHistoryItemData, user: User) -> None:
async with self._uow() as uow:
Expand Down Expand Up @@ -202,8 +215,9 @@ async def generate_conversation_title(self, *, context_id: UUID):
logger.warning(f"Cannot generate title for context {context_id}: no history found.")
return

text, files = self._extract_content_for_title(msg.items[0].data)
if not text and not files:
raw_message = msg.items[0].data
text, title_hint, files = self._extract_content_for_title(raw_message)
if not text and not title_hint and not files:
logger.warning(f"Cannot generate title for context {context_id}: first message has no content.")
return

Expand All @@ -212,7 +226,9 @@ async def generate_conversation_title(self, *, context_id: UUID):
template = Template(self._configuration.generate_conversation_title.prompt)
prompt = template.render(
text=text,
titleHint=title_hint,
files=[file.model_dump(include={"name", "mime_type"}) for file in files],
rawMessage=raw_message.model_dump(),
)
resp = await self._model_provider_service.create_chat_completion(
request=ChatCompletionRequest(
Expand All @@ -222,8 +238,10 @@ async def generate_conversation_title(self, *, context_id: UUID):
messages=[{"role": "user", "content": prompt}],
)
)
title = resp["choices"][0]["message"]["content"].strip().strip("\"'") # pyright: ignore [reportIndexIssue]
title = (resp.choices[0].message.content or "").strip().strip("\"'")
title = f"{title[:100]}..." if len(title) > 100 else title
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current title truncation logic can result in a title longer than 100 characters. For example, if the original title is 101 characters long, it will be truncated to 100 characters and ... will be appended, resulting in a 103-character string. This may violate the 'max 100 characters' constraint mentioned in the prompt and potentially cause issues with database field length limits.

To fix this, you can adjust the truncation to account for the length of the ellipsis.

Suggested change
title = f"{title[:100]}..." if len(title) > 100 else title
title = (title[:97] + "...") if len(title) > 100 else title

if not title:
raise RuntimeError("Generated title is empty.")
async with self._uow() as uow:
await uow.contexts.update_title(
context_id=context_id, title=title, generation_state=TitleGenerationState.COMPLETED
Expand Down
11 changes: 11 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ a2aProxyRequestsExpireAfterDays: 14
generateConversationTitle:
enabled: true
model: default
# You can use the following variables in prompt:
# - text: all text parts of the user message concatenated
# - files: list of files attached to the user message
# - titleHint: hint for the title extracted from DataPart
# - rawMessage: entire user message object, WARNING: may contain secrets in metadata, be careful not to send them to the LLM

prompt: |
YOUR INSTRUCTIONS:
Write a short descriptive title for the conversation (max 100 characters).
Expand All @@ -34,6 +40,11 @@ generateConversationTitle:

CONVERSATION CONTENT:
```
{% if titleHint %}
Title hint:
{{ titleHint }}
{% endif %}

User message:
{{ text }}

Expand Down