Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5a69b0a
wip: add strands integration core files
statefb Jul 31, 2025
184f834
update deps
statefb Jul 31, 2025
6666daa
wip
statefb Jul 31, 2025
08ed418
update firecrawl-py version
statefb Aug 1, 2025
804ed54
insert debug logs
statefb Aug 1, 2025
280ff15
fix: on_stop impl
statefb Aug 1, 2025
6c6719f
wip
statefb Aug 1, 2025
a0ae60c
Fix reasoning content extraction from Strands AgentResult
statefb Aug 1, 2025
88f9085
fix: tool use id conversion
statefb Aug 2, 2025
f868e5d
fix: internet
statefb Aug 4, 2025
bb004a2
add debug log on websocket.py
statefb Aug 5, 2025
f4b54b7
add pytest
statefb Aug 5, 2025
045817b
add debug log on usePostMessageStreaming
statefb Aug 5, 2025
81eceb7
fix: tool input / output not displayed
statefb Aug 5, 2025
bcb27d5
fix: reasoning not persist
statefb Aug 5, 2025
af402d6
add calc tool for testing
statefb Aug 5, 2025
fc213a2
fix: multi turn conversation
statefb Aug 5, 2025
4012b5a
fix: tool registry
statefb Aug 6, 2025
9093e40
fix: wait complete tool input
statefb Aug 6, 2025
3399dca
fix: citation
statefb Aug 6, 2025
b54449a
fix: tool registry
statefb Aug 6, 2025
be189d4
fix: tool input consistency
statefb Aug 6, 2025
810a700
fix: support list
statefb Aug 7, 2025
19f7ac5
fix: list citation
statefb Aug 7, 2025
46f9385
fix: citation
statefb Aug 7, 2025
19c4640
remove context
statefb Aug 7, 2025
733d7a9
fix: knowledge tool strands to return list
statefb Aug 7, 2025
07d1798
refactor
statefb Aug 7, 2025
73c77ba
update strands version
statefb Aug 7, 2025
72def29
wip: chat strands refactor
statefb Aug 7, 2025
8e92411
refactor: call back handler
statefb Aug 7, 2025
13503b2
add post processing
statefb Aug 7, 2025
56f81c5
fix: tools / utils
statefb Aug 8, 2025
75b4ff4
fix: attatchment docs
statefb Aug 8, 2025
28ca754
fix: image content
statefb Aug 8, 2025
7187fbe
fix: continue generation
statefb Aug 8, 2025
d1ef26f
fix: Skip instruction messages as they are handled separately via mes…
statefb Aug 8, 2025
64a2f36
change log level for websocket.py
statefb Aug 8, 2025
8f2f322
lint and add comment
statefb Aug 8, 2025
47a594b
remove deprecated refactorings
statefb Aug 12, 2025
d35a229
fix: unittest
statefb Aug 12, 2025
86bee54
fix tools to return result as strands formats
statefb Aug 12, 2025
b3471a6
rename modules
statefb Aug 12, 2025
078b767
fix: skip reasoning / tool content to construct strunds message befor…
statefb Aug 13, 2025
4d207b6
fix: knowledge_search
statefb Aug 14, 2025
6700468
fix: source id citation
statefb Aug 15, 2025
39a9bc6
add: prompt cache (system, tool)
statefb Aug 28, 2025
2ce970d
add message cache
statefb Aug 28, 2025
54508b8
insert debug log
statefb Aug 28, 2025
3f9d42c
return empty list when no tool
statefb Aug 28, 2025
72181ee
fix: tool util
statefb Aug 29, 2025
770ec40
fix bedrock agent tool
statefb Aug 29, 2025
ae62ab2
add deprecated decorator
statefb Aug 29, 2025
fffef05
update documents including examples
statefb Aug 29, 2025
cd59fd6
add deprication decorator
statefb Aug 29, 2025
0f1d7bc
switch fetch_available_agent_tools for strands
statefb Aug 29, 2025
cf031d8
refactor modules for readability
statefb Aug 29, 2025
603a486
chore: mypy
statefb Aug 29, 2025
a768b83
refactor: simplify on_stop lambda in process_chat_input
statefb Aug 29, 2025
61fd258
remove unused imports on routes/bot.py
statefb Sep 1, 2025
19f20ec
fix: support legacy for `fetch_available_agent_tools`
statefb Sep 1, 2025
496e446
chore: lint
statefb Sep 1, 2025
1e13c42
remove unused tests
statefb Sep 1, 2025
54b5398
feat: implement telemetry management and data extraction for Strands …
statefb Sep 2, 2025
7c0868f
convert relative import to absolute
statefb Sep 3, 2025
dac0589
refactor: reorganize imports and remove console exporter setup
statefb Sep 3, 2025
2bc6450
Merge branch 'v3' into refactor-strands
statefb Sep 3, 2025
b29246b
add type notation
statefb Sep 3, 2025
1c25b62
fix: change back `bedrock_agent_invoke` to original: `bedrock_agent`
statefb Sep 3, 2025
2a81333
fix: change back `knowledge_search` to original: `knowledge_base_tool`
statefb Sep 3, 2025
83ce685
chore: lint
statefb Sep 3, 2025
1b20fd8
fix: tool example
statefb Sep 4, 2025
8c19b5f
add RAG support for model which does not support tool
statefb Sep 4, 2025
c8614f7
Merge branch 'v3' into refactor-strands
Yukinobu-Mine Sep 18, 2025
1263fa2
Merge branch 'v3' into refactor-strands
Yukinobu-Mine Sep 19, 2025
48a1ea5
Refactor implementation of strands agents migration.
Yukinobu-Mine Sep 16, 2025
a8946b3
refactor: add _ prefix to local helper functions in strands converters
statefb Oct 14, 2025
9728684
Merge branch 'v3' into refactor-strands
statefb Oct 14, 2025
fdd687a
chore: lint
statefb Oct 15, 2025
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
8 changes: 3 additions & 5 deletions backend/app/agents/tools/agent_tool.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
from typing import Any, Callable, Generic, Literal, TypedDict, TypeVar

from app.repositories.models.conversation import (
ToolResultModel,
TextToolResultModel,
JsonToolResultModel,
RelatedDocumentModel,
TextToolResultModel,
ToolResultModel,
)
from app.repositories.models.custom_bot import BotModel
from app.routes.schemas.conversation import type_model_name
from mypy_boto3_bedrock_runtime.type_defs import ToolSpecificationTypeDef
from pydantic import BaseModel, JsonValue
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
from mypy_boto3_bedrock_runtime.type_defs import (
ToolSpecificationTypeDef,
)

T = TypeVar("T", bound=BaseModel)

Expand Down
108 changes: 108 additions & 0 deletions backend/app/agents/tools/calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Calculator tool for mathematical calculations.
The purpose of this tool is for testing.
"""

import logging
import re
from typing import Any

from app.agents.tools.agent_tool import AgentTool
from app.repositories.models.custom_bot import BotModel
from app.routes.schemas.conversation import type_model_name
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class CalculatorInput(BaseModel):
expression: str = Field(
description="Mathematical expression to evaluate (e.g., '2+2', '10*5', '100/4')"
)


def calculate_expression(expression: str) -> str:
"""
Safely evaluate a mathematical expression.

Args:
expression: Mathematical expression to evaluate

Returns:
str: Result of the calculation or error message
"""
logger.info(f"[CALCULATOR_TOOL] Calculating expression: {expression}")

try:
# Clean the expression - remove spaces
cleaned_expression = expression.replace(" ", "")
logger.debug(f"[CALCULATOR_TOOL] Cleaned expression: {cleaned_expression}")

# Validate expression contains only allowed characters
if not re.match(r"^[0-9+\-*/().]+$", cleaned_expression):
logger.warning(
f"[CALCULATOR_TOOL] Invalid characters in expression: {expression}"
)
return "Error: Invalid characters in expression. Only numbers and basic operators (+, -, *, /, parentheses) are allowed."

# Check for division by zero
if "/0" in cleaned_expression:
logger.error(
f"[CALCULATOR_TOOL] Division by zero in expression: {expression}"
)
return "Error: Division by zero is not allowed."

# Safely evaluate the expression
result = eval(cleaned_expression)
logger.debug(f"[CALCULATOR_TOOL] Calculation result: {result}")

# Format the result
if isinstance(result, float) and result.is_integer():
formatted_result = str(int(result))
else:
formatted_result = str(result)

logger.debug(f"[CALCULATOR_TOOL] Formatted result: {formatted_result}")
return formatted_result

except ZeroDivisionError:
logger.error(f"[CALCULATOR_TOOL] Division by zero in expression: {expression}")
return "Error: Division by zero is not allowed."
except Exception as e:
logger.error(
f"[CALCULATOR_TOOL] Error calculating expression '{expression}': {e}"
)
return f"Error: Unable to calculate the expression. Please check the syntax."


def _calculator_function(
input_data: CalculatorInput,
bot: BotModel | None,
model: type_model_name | None,
) -> str:
"""
Calculator tool function for AgentTool.

Args:
input_data: Calculator input containing the expression
bot: Bot model (not used for calculator)
model: Model name (not used for calculator)

Returns:
str: Calculation result
"""
return calculate_expression(input_data.expression)


# Backward compatibility alias
_calculate_expression = calculate_expression


# Create the calculator tool instance
calculator_tool = AgentTool(
name="calculator",
description="Perform mathematical calculations like addition, subtraction, multiplication, and division",
args_schema=CalculatorInput,
function=_calculator_function,
)
132 changes: 102 additions & 30 deletions backend/app/agents/tools/internet_search.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import logging
import json
import logging

from app.agents.tools.agent_tool import AgentTool
from app.repositories.models.custom_bot import BotModel, InternetToolModel
from app.routes.schemas.conversation import type_model_name
from app.utils import get_bedrock_runtime_client
from duckduckgo_search import DDGS
from firecrawl.firecrawl import FirecrawlApp
from firecrawl import FirecrawlApp
from pydantic import BaseModel, Field, root_validator

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -138,48 +138,104 @@ def _search_with_firecrawl(
try:
app = FirecrawlApp(api_key=api_key)

# Search using Firecrawl
# SearchParams: https://github.com/mendableai/firecrawl/blob/main/apps/python-sdk/firecrawl/firecrawl.py#L24
from firecrawl import ScrapeOptions

# Incoming locale is language-country (e.g. 'en-us').
language, country = locale.split("-", 1)
results = app.search(
query,
{
"limit": max_results,
"lang": language,
"location": country,
"scrapeOptions": {"formats": ["markdown"], "onlyMainContent": True},
},
limit=max_results,
lang=language,
location=country,
scrape_options=ScrapeOptions(formats=["markdown"], onlyMainContent=True),
)

if not results:
logger.warning("No results found")
return []
logger.info(f"results of firecrawl: {results}")

# Log detailed information about the results object
logger.info(
f"results of firecrawl: success={getattr(results, 'success', 'unknown')} warning={getattr(results, 'warning', None)} error={getattr(results, 'error', None)}"
)

# Log the data structure
if hasattr(results, "data"):
data_sample = results.data[:1] if results.data else []
logger.info(f"data sample: {data_sample}")
else:
logger.info(
f"results attributes: {[attr for attr in dir(results) if not attr.startswith('_')]}"
)
logger.info(
f"results as dict attempt: {dict(results) if hasattr(results, '__dict__') else 'no __dict__'}"
)

# Format and summarize search results
search_results = []
for data in results.get("data", []):
if isinstance(data, dict):
title = data.get("title", "")
url = data.get("metadata", {}).get("sourceURL", "")
content = data.get("markdown", {})

# Summarize the content
summary = _summarize_content(content, title, url, query)

search_results.append(
{
"content": summary,
"source_name": title,
"source_link": url,
}

# Handle Firecrawl SearchResponse object structure
# The Python SDK returns a SearchResponse object with .data attribute
if hasattr(results, "data") and results.data:
data_list = results.data
else:
logger.error(
f"No data found in results. Results type: {type(results)}, attributes: {[attr for attr in dir(results) if not attr.startswith('_')]}"
)
return []

logger.info(f"Found {len(data_list)} data items")
for i, data in enumerate(data_list):
try:
logger.info(
f"Data item {i}: type={type(data)}, keys={list(data.keys()) if isinstance(data, dict) else 'not dict'}"
)

if isinstance(data, dict):
title = data.get("title", "")
# Try different URL fields based on Firecrawl API response structure
url = data.get("url", "") or (
data.get("metadata", {}).get("sourceURL", "")
if isinstance(data.get("metadata"), dict)
else ""
)
content = data.get("markdown", "") or data.get("content", "")

if not title and not content:
logger.warning(f"Skipping data item {i} - no title or content")
continue

# Summarize the content
summary = _summarize_content(content, title, url, query)

search_results.append(
{
"content": summary,
"source_name": title,
"source_link": url,
}
)
else:
logger.warning(f"Data item {i} is not a dict: {type(data)}")
except Exception as e:
logger.error(f"Error processing data item {i}: {e}")
continue

logger.info(f"Found {len(search_results)} results from Firecrawl")
return search_results

except Exception as e:
logger.error(f"Error searching with Firecrawl: {e}")
raise e
logger.error(f"Exception type: {type(e)}")
logger.error(f"Exception args: {e.args}")
import traceback

logger.error(f"Traceback: {traceback.format_exc()}")

# Instead of raising, return empty list to allow fallback
return []


def _internet_search(
Expand Down Expand Up @@ -211,22 +267,38 @@ def _internet_search(
# Handle Firecrawl search
if internet_tool.search_engine == "firecrawl":
if not internet_tool.firecrawl_config:
raise ValueError("Firecrawl configuration is not set in the bot.")
logger.error(
"Firecrawl configuration is not set in the bot, falling back to DuckDuckGo"
)
return _search_with_duckduckgo(query, time_limit, locale)

try:
api_key = internet_tool.firecrawl_config.api_key
if not api_key:
raise ValueError("Firecrawl API key is empty")
logger.error("Firecrawl API key is empty, falling back to DuckDuckGo")
return _search_with_duckduckgo(query, time_limit, locale)

return _search_with_firecrawl(
results = _search_with_firecrawl(
query=query,
api_key=api_key,
locale=locale,
max_results=internet_tool.firecrawl_config.max_results,
)

# If Firecrawl returns empty results, fallback to DuckDuckGo
if not results:
logger.warning(
"Firecrawl returned no results, falling back to DuckDuckGo"
)
return _search_with_duckduckgo(query, time_limit, locale)

return results

except Exception as e:
logger.error(f"Error with Firecrawl search: {e}")
raise e
logger.error(
f"Error with Firecrawl search: {e}, falling back to DuckDuckGo"
)
return _search_with_duckduckgo(query, time_limit, locale)

# Fallback to DuckDuckGo for any unexpected cases
logger.warning("Unexpected search engine configuration, falling back to DuckDuckGo")
Expand Down
Loading