Skip to content

Commit d0248d8

Browse files
authored
feat(chat-agent): add citation and trajectory extensions (#1000)
Signed-off-by: Aleš Kalfas <kalfas.ales@gmail.com>
1 parent 4d404b6 commit d0248d8

File tree

6 files changed

+173
-34
lines changed

6 files changed

+173
-34
lines changed

agents/official/beeai-framework/chat/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = [
77
]
88
requires-python = ">=3.13,<4"
99
dependencies = [
10-
"beeai-framework[duckduckgo,wikipedia]~=0.1.31",
10+
"beeai-framework[duckduckgo,wikipedia]~=0.1.34",
1111
"beeai-sdk",
1212
"openinference-instrumentation-beeai>=0.1.6",
1313
"pydantic-settings>=2.9.0",

agents/official/beeai-framework/chat/src/chat/agent.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
# SPDX-License-Identifier: Apache-2.0
33
import logging
44
import os
5+
from typing import Annotated
56
import uuid
67
from collections import defaultdict
78
from textwrap import dedent
89

910
from a2a.types import (
11+
AgentCapabilities,
1012
AgentSkill,
1113
Artifact,
1214
FilePart,
@@ -21,6 +23,7 @@
2123
from beeai_framework.agents.experimental.events import (
2224
RequirementAgentSuccessEvent,
2325
)
26+
from beeai_framework.agents.experimental.utils._tool import FinalAnswerTool
2427
from beeai_framework.backend.types import ChatModelParameters
2528
from beeai_framework.memory import UnconstrainedMemory
2629
from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware
@@ -29,10 +32,19 @@
2932
from beeai_framework.tools.search.wikipedia import WikipediaTool
3033
from beeai_framework.tools.weather.openmeteo import OpenMeteoTool
3134

32-
from beeai_sdk.a2a.extensions import AgentDetail, AgentDetailTool
35+
from beeai_sdk.a2a.extensions import (
36+
AgentDetail,
37+
AgentDetailTool,
38+
CitationExtensionServer,
39+
CitationExtensionSpec,
40+
TrajectoryExtensionServer,
41+
TrajectoryExtensionSpec,
42+
)
3343
from beeai_sdk.a2a.types import AgentMessage
3444
from beeai_sdk.server import Server
3545
from beeai_sdk.server.context import Context
46+
from chat.helpers.citations import extract_citations
47+
from chat.helpers.trajectory import TrajectoryContent
3648
from openinference.instrumentation.beeai import BeeAIInstrumentor
3749

3850
from chat.tools.files.file_creator import FileCreatorTool, FileCreatorToolOutput
@@ -51,9 +63,12 @@
5163

5264
BeeAIInstrumentor().instrument()
5365
## TODO: https://github.com/phoenixframework/phoenix/issues/6224
54-
logging.getLogger("opentelemetry.exporter.otlp.proto.http._log_exporter").setLevel(logging.CRITICAL)
55-
logging.getLogger("opentelemetry.exporter.otlp.proto.http.metric_exporter").setLevel(logging.CRITICAL)
56-
66+
logging.getLogger("opentelemetry.exporter.otlp.proto.http._log_exporter").setLevel(
67+
logging.CRITICAL
68+
)
69+
logging.getLogger("opentelemetry.exporter.otlp.proto.http.metric_exporter").setLevel(
70+
logging.CRITICAL
71+
)
5772

5873
logger = logging.getLogger(__name__)
5974

@@ -76,9 +91,17 @@
7691
ui_type="chat",
7792
user_greeting="How can I help you?",
7893
tools=[
79-
AgentDetailTool(name="Web Search (DuckDuckGo)", description="Retrieves real-time search results."),
80-
AgentDetailTool(name="Wikipedia Search", description="Fetches summaries from Wikipedia."),
81-
AgentDetailTool(name="Weather Information (OpenMeteo)", description="Provides real-time weather updates."),
94+
AgentDetailTool(
95+
name="Web Search (DuckDuckGo)",
96+
description="Retrieves real-time search results.",
97+
),
98+
AgentDetailTool(
99+
name="Wikipedia Search", description="Fetches summaries from Wikipedia."
100+
),
101+
AgentDetailTool(
102+
name="Weather Information (OpenMeteo)",
103+
description="Provides real-time weather updates.",
104+
),
82105
],
83106
framework="BeeAI",
84107
),
@@ -114,23 +137,44 @@
114137
"""
115138
),
116139
tags=["chat"],
117-
examples=["Please find a room in LA, CA, April 15, 2025, checkout date is april 18, 2 adults"],
140+
examples=[
141+
"Please find a room in LA, CA, April 15, 2025, checkout date is april 18, 2 adults"
142+
],
118143
)
119144
],
120145
)
121-
async def chat(message: Message, context: Context):
146+
async def chat(
147+
message: Message,
148+
context: Context,
149+
trajectory: Annotated[TrajectoryExtensionServer, TrajectoryExtensionSpec()],
150+
citation: Annotated[CitationExtensionServer, CitationExtensionSpec()],
151+
):
122152
"""
123153
The agent is an AI-powered conversational system with memory, supporting real-time search, Wikipedia lookups,
124154
and weather updates through integrated tools.
125155
"""
126-
extracted_files = await extract_files(history=messages[context.context_id], incoming_message=message)
156+
extracted_files = await extract_files(
157+
history=messages[context.context_id], incoming_message=message
158+
)
127159
input = to_framework_message(message)
128160

129161
# Configure tools
130162
file_reader_tool_class = create_file_reader_tool_class(
131163
extracted_files
132164
) # Dynamically created tool input schema based on real provided files ensures that small LLMs can't hallucinate the input
133165

166+
FinalAnswerTool.description = """Assemble and send the final answer to the user. When using information gathered from other tools that provided URL addresses, you MUST properly cite them using markdown citation format: [description](URL).
167+
168+
Citation Requirements:
169+
- Use descriptive text that summarizes the source content
170+
- Include the exact URL provided by the tool
171+
- Place citations inline where the information is referenced
172+
173+
Examples:
174+
- According to [OpenAI's latest announcement](https://example.com/gpt5), GPT-5 will be released next year.
175+
- Recent studies show [AI adoption has increased by 67%](https://example.com/ai-study) in enterprise environments.
176+
- Weather data indicates [temperatures will reach 25°C tomorrow](https://weather.example.com/forecast).""" # type: ignore
177+
134178
tools = [
135179
# Auxiliary tools
136180
ActTool(), # Enforces correct thinking sequence by requiring tool selection before execution
@@ -180,6 +224,16 @@ async def chat(message: Message, context: Context):
180224

181225
last_step = event.state.steps[-1] if event.state.steps else None
182226
if last_step and last_step.tool is not None:
227+
trajectory_content = TrajectoryContent(
228+
input=last_step.input,
229+
output=last_step.output,
230+
error=last_step.error,
231+
)
232+
yield trajectory.trajectory_metadata(
233+
title=last_step.tool.name,
234+
content=trajectory_content.model_dump_json(),
235+
)
236+
183237
if isinstance(last_step.output, FileCreatorToolOutput):
184238
result = last_step.output.result
185239
for file_info in result.files:
@@ -205,7 +259,15 @@ async def chat(message: Message, context: Context):
205259

206260
if final_answer:
207261
framework_messages[context.context_id].append(final_answer)
208-
message = AgentMessage(text=final_answer.text)
262+
263+
citations, clean_text = extract_citations(final_answer.text)
264+
265+
message = AgentMessage(
266+
text=clean_text,
267+
metadata=(
268+
citation.citation_metadata(citations=citations) if citations else None
269+
),
270+
)
209271
messages[context.context_id].append(message)
210272
yield message
211273

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import re
5+
from beeai_sdk.a2a.extensions import Citation
6+
7+
8+
def extract_citations(text: str) -> tuple[list[Citation], str]:
9+
"""
10+
Extract citations from markdown-style links and return cleaned text.
11+
12+
This function parses text containing markdown-style citations in the format
13+
[citation_text](url) and extracts them into Citation objects while cleaning
14+
the original text to contain only the citation content.
15+
16+
Args:
17+
text (str): Input text containing markdown-style citations
18+
19+
Returns:
20+
tuple[list[Citation], str]: A tuple containing:
21+
- List of Citation objects with metadata
22+
- Cleaned text with citation links replaced by content only
23+
24+
Example:
25+
>>> text = "According to [recent studies](https://example.com/study) and [research papers](https://academic.org/paper), AI is advancing rapidly."
26+
>>> citations, clean_text = extract_citations(text)
27+
>>> print(clean_text)
28+
"According to recent studies and research papers, AI is advancing rapidly."
29+
>>> print(len(citations))
30+
2
31+
>>> print(citations[0].url)
32+
"https://example.com/study"
33+
>>> print(citations[0].title)
34+
"Study"
35+
>>> print(citations[0].description)
36+
"recent studies"
37+
"""
38+
citations, offset = [], 0
39+
pattern = r"\[([^\]]+)\]\(([^)]+)\)"
40+
41+
for match in re.finditer(pattern, text):
42+
content, url = match.groups()
43+
start = match.start() - offset
44+
45+
citations.append(
46+
Citation(
47+
url=url,
48+
title=url.split("/")[-1].replace("-", " ").title() or content[:50],
49+
description=content[:100] + ("..." if len(content) > 100 else ""),
50+
start_index=start,
51+
end_index=start + len(content),
52+
)
53+
)
54+
offset += len(match.group(0)) - len(content)
55+
56+
return citations, re.sub(pattern, r"\1", text)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from typing import Any
5+
from beeai_framework.errors import FrameworkError
6+
from beeai_framework.tools import ToolOutput
7+
from pydantic import BaseModel, InstanceOf, field_serializer
8+
9+
class TrajectoryContent(BaseModel):
10+
input: Any
11+
output: InstanceOf[ToolOutput] | None = None
12+
error: InstanceOf[FrameworkError] | None = None
13+
14+
@field_serializer('output')
15+
def serialize_output(self, output: ToolOutput | None) -> Any:
16+
if output is None:
17+
return None
18+
# Check if it's a JSONToolOutput with to_json_safe method
19+
if hasattr(output, 'to_json_safe'):
20+
return output.to_json_safe()
21+
# Fallback to text content for other ToolOutput types
22+
return {"text_content": output.get_text_content()}
23+
24+
@field_serializer('error')
25+
def serialize_error(self, error: FrameworkError | None) -> dict[str, Any] | None:
26+
if error is None:
27+
return None
28+
return {"message": str(error), "type": error.__class__.__name__}

agents/official/beeai-framework/chat/src/chat/tools/general/act.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,15 @@ async def run(self, state: RequirementAgentRunState, ctx: RunContext) -> list[Ru
162162
"Last step output must be an instance of ActToolOutput."
163163
)
164164
selected_tool = last_step.output.result.selected_tool
165-
if selected_tool == "final_answer":
166-
return []
167-
else:
168-
return [
169-
Rule(
170-
target=selected_tool,
171-
forced=True,
172-
allowed=True,
173-
prevent_stop=False,
174-
hidden=False,
175-
)
176-
]
165+
return [
166+
Rule(
167+
target=selected_tool,
168+
forced=True,
169+
allowed=True,
170+
prevent_stop=False,
171+
hidden=False,
172+
)
173+
]
177174

178175
return [
179176
Rule(
@@ -197,11 +194,7 @@ def act_tool_middleware(ctx: RunContext) -> None:
197194
raise ValueError("ActTool is not found in the agent's tools.")
198195

199196
def handle_start(data: RequirementAgentStartEvent, event: EventMeta) -> None:
200-
allowed_tools = (
201-
[t.name for t in data.request.tools if t.name != "act"]
202-
if data.state.iteration == 1
203-
else [t.name for t in data.request.allowed_tools if t.name != "act"]
204-
)
197+
allowed_tools = [t.name for t in data.request.tools if t.name != "act"]
205198
act_tool.allowed_tools_names = allowed_tools
206199

207200
ctx.emitter.on("start", handle_start)

agents/official/beeai-framework/chat/uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)