From 466652c847c3354d2c234e3618dc0aa5c90ff50a Mon Sep 17 00:00:00 2001 From: "wuqingfu.528" Date: Mon, 9 Feb 2026 16:11:32 +0800 Subject: [PATCH 1/6] feat: support vanna tools --- veadk/tools/vanna_tools/agent_memory.py | 310 +++++++++++++++ veadk/tools/vanna_tools/examples/agent.py | 307 +++++++++++++++ veadk/tools/vanna_tools/file_system.py | 453 ++++++++++++++++++++++ veadk/tools/vanna_tools/python.py | 216 +++++++++++ veadk/tools/vanna_tools/run_sql.py | 109 ++++++ veadk/tools/vanna_tools/summarize_data.py | 110 ++++++ veadk/tools/vanna_tools/visualize_data.py | 109 ++++++ 7 files changed, 1614 insertions(+) create mode 100644 veadk/tools/vanna_tools/agent_memory.py create mode 100644 veadk/tools/vanna_tools/examples/agent.py create mode 100644 veadk/tools/vanna_tools/file_system.py create mode 100644 veadk/tools/vanna_tools/python.py create mode 100644 veadk/tools/vanna_tools/run_sql.py create mode 100644 veadk/tools/vanna_tools/summarize_data.py create mode 100644 veadk/tools/vanna_tools/visualize_data.py diff --git a/veadk/tools/vanna_tools/agent_memory.py b/veadk/tools/vanna_tools/agent_memory.py new file mode 100644 index 00000000..9c02502e --- /dev/null +++ b/veadk/tools/vanna_tools/agent_memory.py @@ -0,0 +1,310 @@ +from typing import Any, Dict, Optional, List +from google.adk.tools import BaseTool, ToolContext +from google.genai import types +from vanna.tools.agent_memory import ( + SaveQuestionToolArgsTool as VannaSaveQuestionToolArgsTool, + SearchSavedCorrectToolUsesTool as VannaSearchSavedCorrectToolUsesTool, + SaveTextMemoryTool as VannaSaveTextMemoryTool, +) +from vanna.core.user import User +from vanna.core.tool import ToolContext as VannaToolContext + + +class SaveQuestionToolArgsTool(BaseTool): + """Save successful question-tool-argument combinations for future reference.""" + + def __init__( + self, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the save tool usage tool with custom agent_memory. + + Args: + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + access_groups: List of user groups that can access this tool (e.g., ['admin']) + """ + self.agent_memory = agent_memory + self.vanna_tool = VannaSaveQuestionToolArgsTool() + self.access_groups = access_groups or ["admin"] # Default: only admin + + super().__init__( + name="save_question_tool_args", # Keep the same name as Vanna + description="Save a successful question-tool-argument combination for future reference.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "question": types.Schema( + type=types.Type.STRING, + description="The original question that was asked", + ), + "tool_name": types.Schema( + type=types.Type.STRING, + description="The name of the tool that was used successfully", + ), + "args": types.Schema( + type=types.Type.OBJECT, + description="The arguments that were passed to the tool", + ), + }, + required=["question", "tool_name", "args"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + """Get user groups from context.""" + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + """Check if user has access to this tool.""" + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + """Create Vanna context from Veadk ToolContext.""" + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + + vanna_context = VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + return vanna_context + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + """Save a tool usage pattern.""" + question = args.get("question", "").strip() + tool_name = args.get("tool_name", "").strip() + tool_args = args.get("args", {}) + + if not question: + return "Error: No question provided" + + if not tool_name: + return "Error: No tool name provided" + + try: + user_groups = self._get_user_groups(tool_context) + + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + + args_model = self.vanna_tool.get_args_schema()( + question=question, tool_name=tool_name, args=tool_args + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + + return str(result.result_for_llm) + except Exception as e: + return f"Error saving tool usage: {str(e)}" + + +class SearchSavedCorrectToolUsesTool(BaseTool): + """Search for similar tool usage patterns based on a question.""" + + def __init__( + self, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the search similar tools tool with custom agent_memory. + + Args: + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + access_groups: List of user groups that can access this tool (e.g., ['admin', 'user']) + user_group_resolver: Optional callable that takes ToolContext and returns user groups + """ + self.agent_memory = agent_memory + self.vanna_tool = VannaSearchSavedCorrectToolUsesTool() + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="search_saved_correct_tool_uses", # Keep the same name as Vanna + description="Search for similar tool usage patterns based on a question.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "question": types.Schema( + type=types.Type.STRING, + description="The question to find similar tool usage patterns for", + ), + "limit": types.Schema( + type=types.Type.INTEGER, + description="Maximum number of results to return (default: 10)", + ), + }, + required=["question"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + """Get user groups from context.""" + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + """Check if user has access to this tool.""" + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + """Create Vanna context from Veadk ToolContext.""" + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + + vanna_context = VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + return vanna_context + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + """Search for similar tool usage patterns.""" + question = args.get("question", "").strip() + limit = args.get("limit", 10) + + if not question: + return "Error: No question provided" + + try: + user_groups = self._get_user_groups(tool_context) + + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + + args_model = self.vanna_tool.get_args_schema()( + question=question, limit=limit + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + + return str(result.result_for_llm) + except Exception as e: + return f"Error searching similar tools: {str(e)}" + + +class SaveTextMemoryTool(BaseTool): + """Save free-form text memories for important insights, observations, or context.""" + + def __init__( + self, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the save text memory tool with custom agent_memory. + + Args: + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + access_groups: List of user groups that can access this tool (e.g., ['admin', 'user']) + user_group_resolver: Optional callable that takes ToolContext and returns user groups + """ + self.agent_memory = agent_memory + self.vanna_tool = VannaSaveTextMemoryTool() + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="save_text_memory", # Keep the same name as Vanna + description="Save free-form text memory for important insights, observations, or context.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "content": types.Schema( + type=types.Type.STRING, + description="The text content to save as a memory", + ), + }, + required=["content"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + """Get user groups from context.""" + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + """Check if user has access to this tool.""" + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + """Create Vanna context from Veadk ToolContext.""" + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + + vanna_context = VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + return vanna_context + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + """Save a text memory.""" + content = args.get("content", "").strip() + + if not content: + return "Error: No content provided" + + try: + user_groups = self._get_user_groups(tool_context) + + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + + args_model = self.vanna_tool.get_args_schema()(content=content) + result = await self.vanna_tool.execute(vanna_context, args_model) + + return str(result.result_for_llm) + except Exception as e: + return f"Error saving text memory: {str(e)}" diff --git a/veadk/tools/vanna_tools/examples/agent.py b/veadk/tools/vanna_tools/examples/agent.py new file mode 100644 index 00000000..a770eafd --- /dev/null +++ b/veadk/tools/vanna_tools/examples/agent.py @@ -0,0 +1,307 @@ +import os +from veadk import Agent, Runner + +# Import Vanna dependencies for initialization +from vanna.integrations.sqlite import SqliteRunner +from vanna.tools import LocalFileSystem +from vanna.integrations.local.agent_memory import DemoAgentMemory +import httpx + +# Import the refactored class-based tools +from veadk.tools.vanna_tools.run_sql import RunSqlTool +from veadk.tools.vanna_tools.visualize_data import VisualizeDataTool +from veadk.tools.vanna_tools.file_system import WriteFileTool +from veadk.tools.vanna_tools.agent_memory import ( + SaveQuestionToolArgsTool, + SearchSavedCorrectToolUsesTool, +) +from veadk.tools.vanna_tools.summarize_data import SummarizeDataTool + +from google.adk.sessions import InMemorySessionService + + +# Setup SQLite database +def setup_sqlite(): + """Download and setup the Chinook SQLite database.""" + db_path = "/tmp/Chinook.sqlite" + if not os.path.exists(db_path): + print("Downloading Chinook.sqlite...") + url = "https://vanna.ai/Chinook.sqlite" + try: + with open(db_path, "wb") as f: + with httpx.stream("GET", url) as response: + for chunk in response.iter_bytes(): + f.write(chunk) + print("Database downloaded successfully!") + except Exception as e: + print(f"Error downloading database: {e}") + return db_path + + +async def create_session(user_groups: list = ["user"]): + session_service = InMemorySessionService() + example_session = await session_service.create_session( + app_name="example_app", + user_id="example_user", + state={"user_groups": user_groups}, + ) + return session_service, example_session + + +# Initialize user-customizable resources +db_path = setup_sqlite() + +# 1. SQL Runner - can be SqliteRunner, PostgresRunner, MySQLRunner, etc. +sqlite_runner = SqliteRunner(database_path=db_path) + +# 2. File System - customize working directory as needed +file_system = LocalFileSystem(working_directory="/tmp/data_storage") +if not os.path.exists("/tmp/data_storage"): + os.makedirs("/tmp/data_storage", exist_ok=True) + +# 3. Agent Memory - customize memory implementation and capacity +agent_memory = DemoAgentMemory(max_items=1000) + +# Initialize tools with user-defined components and access control +# Tool names now match Vanna's original names for compatibility +run_sql_tool = RunSqlTool( + sql_runner=sqlite_runner, + file_system=file_system, + agent_memory=agent_memory, + access_groups=["admin", "user"], # Both admin and user can use +) + +visualize_data_tool = VisualizeDataTool( + file_system=file_system, + agent_memory=agent_memory, + access_groups=["admin", "user"], +) + +write_file_tool = WriteFileTool( + file_system=file_system, + agent_memory=agent_memory, + access_groups=["admin", "user"], +) + +# Memory tools: save only for admin, search for all users +save_tool = SaveQuestionToolArgsTool( + agent_memory=agent_memory, + access_groups=["admin"], # Only admin can save +) + +search_tool = SearchSavedCorrectToolUsesTool( + agent_memory=agent_memory, + access_groups=["admin", "user"], # All users can search +) + +summarize_data_tool = SummarizeDataTool( + file_system=file_system, + agent_memory=agent_memory, + access_groups=["admin", "user"], +) + +# Define the Veadk Agent with class-based tools +agent: Agent = Agent( + name="vanna_sql_agent", + description="An intelligent agent that can query databases, visualize data, and generate reports.", + instruction=""" + You are a helpful assistant that can answer questions about data in the Chinook database. + You can execute SQL queries, visualize the results, save/search useful tool usage patterns, and generate documents. + + Here is the schema of the Chinook database: + ```sql + CREATE TABLE [Album] + ( + [AlbumId] INTEGER NOT NULL, + [Title] NVARCHAR(160) NOT NULL, + [ArtistId] INTEGER NOT NULL, + CONSTRAINT [PK_Album] PRIMARY KEY ([AlbumId]), + FOREIGN KEY ([ArtistId]) REFERENCES [Artist] ([ArtistId]) + ON DELETE NO ACTION ON UPDATE NO ACTION + ); + CREATE TABLE [Artist] + ( + [ArtistId] INTEGER NOT NULL, + [Name] NVARCHAR(120), + CONSTRAINT [PK_Artist] PRIMARY KEY ([ArtistId]) + ); + CREATE TABLE [Customer] + ( + [CustomerId] INTEGER NOT NULL, + [FirstName] NVARCHAR(40) NOT NULL, + [LastName] NVARCHAR(20) NOT NULL, + [Company] NVARCHAR(80), + [Address] NVARCHAR(70), + [City] NVARCHAR(40), + [State] NVARCHAR(40), + [Country] NVARCHAR(40), + [PostalCode] NVARCHAR(10), + [Phone] NVARCHAR(24), + [Fax] NVARCHAR(24), + [Email] NVARCHAR(60) NOT NULL, + [SupportRepId] INTEGER, + CONSTRAINT [PK_Customer] PRIMARY KEY ([CustomerId]), + FOREIGN KEY ([SupportRepId]) REFERENCES [Employee] ([EmployeeId]) + ON DELETE NO ACTION ON UPDATE NO ACTION + ); + CREATE TABLE [Employee] + ( + [EmployeeId] INTEGER NOT NULL, + [LastName] NVARCHAR(20) NOT NULL, + [FirstName] NVARCHAR(20) NOT NULL, + [Title] NVARCHAR(30), + [ReportsTo] INTEGER, + [BirthDate] DATETIME, + [HireDate] DATETIME, + [Address] NVARCHAR(70), + [City] NVARCHAR(40), + [State] NVARCHAR(40), + [Country] NVARCHAR(40), + [PostalCode] NVARCHAR(10), + [Phone] NVARCHAR(24), + [Fax] NVARCHAR(24), + [Email] NVARCHAR(60), + CONSTRAINT [PK_Employee] PRIMARY KEY ([EmployeeId]), + FOREIGN KEY ([ReportsTo]) REFERENCES [Employee] ([EmployeeId]) + ON DELETE NO ACTION ON UPDATE NO ACTION + ); + CREATE TABLE [Genre] + ( + [GenreId] INTEGER NOT NULL, + [Name] NVARCHAR(120), + CONSTRAINT [PK_Genre] PRIMARY KEY ([GenreId]) + ); + CREATE TABLE [Invoice] + ( + [InvoiceId] INTEGER NOT NULL, + [CustomerId] INTEGER NOT NULL, + [InvoiceDate] DATETIME NOT NULL, + [BillingAddress] NVARCHAR(70), + [BillingCity] NVARCHAR(40), + [BillingState] NVARCHAR(40), + [BillingCountry] NVARCHAR(40), + [BillingPostalCode] NVARCHAR(10), + [Total] NUMERIC(10,2) NOT NULL, + CONSTRAINT [PK_Invoice] PRIMARY KEY ([InvoiceId]), + FOREIGN KEY ([CustomerId]) REFERENCES [Customer] ([CustomerId]) + ON DELETE NO ACTION ON UPDATE NO ACTION + ); + CREATE TABLE [InvoiceLine] + ( + [InvoiceLineId] INTEGER NOT NULL, + [InvoiceId] INTEGER NOT NULL, + [TrackId] INTEGER NOT NULL, + [UnitPrice] NUMERIC(10,2) NOT NULL, + [Quantity] INTEGER NOT NULL, + CONSTRAINT [PK_InvoiceLine] PRIMARY KEY ([InvoiceLineId]), + FOREIGN KEY ([InvoiceId]) REFERENCES [Invoice] ([InvoiceId]) + ON DELETE NO ACTION ON UPDATE NO ACTION, + FOREIGN KEY ([TrackId]) REFERENCES [Track] ([TrackId]) + ON DELETE NO ACTION ON UPDATE NO ACTION + ); + CREATE TABLE [MediaType] + ( + [MediaTypeId] INTEGER NOT NULL, + [Name] NVARCHAR(120), + CONSTRAINT [PK_MediaType] PRIMARY KEY ([MediaTypeId]) + ); + CREATE TABLE [Playlist] + ( + [PlaylistId] INTEGER NOT NULL, + [Name] NVARCHAR(120), + CONSTRAINT [PK_Playlist] PRIMARY KEY ([PlaylistId]) + ); + CREATE TABLE [PlaylistTrack] + ( + [PlaylistId] INTEGER NOT NULL, + [TrackId] INTEGER NOT NULL, + CONSTRAINT [PK_PlaylistTrack] PRIMARY KEY ([PlaylistId], [TrackId]), + FOREIGN KEY ([PlaylistId]) REFERENCES [Playlist] ([PlaylistId]) + ON DELETE NO ACTION ON UPDATE NO ACTION, + FOREIGN KEY ([TrackId]) REFERENCES [Track] ([TrackId]) + ON DELETE NO ACTION ON UPDATE NO ACTION + ); + CREATE TABLE [Track] + ( + [TrackId] INTEGER NOT NULL, + [Name] NVARCHAR(200) NOT NULL, + [AlbumId] INTEGER, + [MediaTypeId] INTEGER NOT NULL, + [GenreId] INTEGER, + [Composer] NVARCHAR(220), + [Milliseconds] INTEGER NOT NULL, + [Bytes] INTEGER, + [UnitPrice] NUMERIC(10,2) NOT NULL, + CONSTRAINT [PK_Track] PRIMARY KEY ([TrackId]), + FOREIGN KEY ([AlbumId]) REFERENCES [Album] ([AlbumId]) + ON DELETE NO ACTION ON UPDATE NO ACTION, + FOREIGN KEY ([GenreId]) REFERENCES [Genre] ([GenreId]) + ON DELETE NO ACTION ON UPDATE NO ACTION, + FOREIGN KEY ([MediaTypeId]) REFERENCES [MediaType] ([MediaTypeId]) + ON DELETE NO ACTION ON UPDATE NO ACTION + ); + ``` + + Here are some examples of how to query this database: + + Q: Get all the tracks in the album 'Balls to the Wall'. + A: SELECT * FROM Track WHERE AlbumId = (SELECT AlbumId FROM Album WHERE Title = 'Balls to the Wall') + + Q: Get the total sales for each customer. + A: SELECT c.FirstName, c.LastName, SUM(i.Total) as TotalSales FROM Customer c JOIN Invoice i ON c.CustomerId = i.CustomerId GROUP BY c.CustomerId + + Q: How many tracks are there in each genre? + A: SELECT g.Name, COUNT(t.TrackId) as TrackCount FROM Genre g JOIN Track t ON g.GenreId = t.GenreId GROUP BY g.GenreId + + Available tools (using Vanna's original names): + 1. `run_sql` - Execute SQL queries + 2. `visualize_data` - Create visualizations from CSV files + 3. `write_file` - Save content to files + 4. `save_question_tool_args` - Save successful tool usage patterns (admin only) + 5. `search_saved_correct_tool_uses` - Search for similar tool usage patterns + 6. `summarize_data` - Generate statistical summaries of CSV files + """, + tools=[ + run_sql_tool, + visualize_data_tool, + write_file_tool, + save_tool, + search_tool, + summarize_data_tool, + ], + model_extra_config={"extra_body": {"thinking": {"type": "disabled"}}}, +) + + +async def main(prompt: str, user_groups: list = None) -> str: + session_service, example_session = await create_session( + user_groups + ) # Default to 'user' group if not specified + + runner = Runner( + agent=agent, + app_name=example_session.app_name, + user_id=example_session.user_id, + session_service=session_service, + ) + + response = await runner.run( + messages=prompt, + session_id=example_session.id, + ) + + return response + + +if __name__ == "__main__": + import asyncio + + # print("=== Example 1: Regular User ===") + # user_input = "What are the top 5 selling albums?" + # response = asyncio.run(main(user_input, user_groups=['user'])) + # print(response) + + print("\n=== Example 2: Admin User (can save patterns) ===") + admin_input = "What are the top 5 selling albums?" + response = asyncio.run(main(admin_input, user_groups=["admin"])) + print(response) diff --git a/veadk/tools/vanna_tools/file_system.py b/veadk/tools/vanna_tools/file_system.py new file mode 100644 index 00000000..d5cd8939 --- /dev/null +++ b/veadk/tools/vanna_tools/file_system.py @@ -0,0 +1,453 @@ +from typing import Any, Dict, Optional, List +from google.adk.tools import BaseTool, ToolContext +from google.genai import types +from vanna.tools.file_system import ( + WriteFileTool as VannaWriteFileTool, + ReadFileTool as VannaReadFileTool, + ListFilesTool as VannaListFilesTool, + SearchFilesTool as VannaSearchFilesTool, + EditFileTool as VannaEditFileTool, +) +from vanna.core.user import User +from vanna.core.tool import ToolContext as VannaToolContext + + +class WriteFileTool(BaseTool): + """Write content to a file.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the write file tool with custom file_system. + + Args: + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + file_system: A Vanna file system instance (e.g., LocalFileSystem) + access_groups: List of user groups that can access this tool + """ + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaWriteFileTool(file_system=file_system) + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="write_file", + description="Write content to a file.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "filename": types.Schema( + type=types.Type.STRING, + description="Name of the file to write", + ), + "content": types.Schema( + type=types.Type.STRING, + description="Content to write to the file", + ), + "overwrite": types.Schema( + type=types.Type.BOOLEAN, + description="Whether to overwrite existing files (default: False)", + ), + }, + required=["filename", "content"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + return VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + filename = args.get("filename", "").strip() + content = args.get("content", "") + overwrite = args.get("overwrite", False) + + if not filename: + return "Error: No filename provided" + + try: + user_groups = self._get_user_groups(tool_context) + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + args_model = self.vanna_tool.get_args_schema()( + filename=filename, content=content, overwrite=overwrite + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + return str(result.result_for_llm) + except Exception as e: + return f"Error writing file: {str(e)}" + + +class ReadFileTool(BaseTool): + """Read the contents of a file.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaReadFileTool(file_system=file_system) + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="read_file", + description="Read the contents of a file.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "filename": types.Schema( + type=types.Type.STRING, + description="Name of the file to read", + ), + }, + required=["filename"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + return VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + filename = args.get("filename", "").strip() + + if not filename: + return "Error: No filename provided" + + try: + user_groups = self._get_user_groups(tool_context) + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + args_model = self.vanna_tool.get_args_schema()(filename=filename) + result = await self.vanna_tool.execute(vanna_context, args_model) + return str(result.result_for_llm) + except Exception as e: + return f"Error reading file: {str(e)}" + + +class ListFilesTool(BaseTool): + """List files in a directory.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaListFilesTool(file_system=file_system) + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="list_files", + description="List files in a directory.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "directory": types.Schema( + type=types.Type.STRING, + description="Directory to list (defaults to current directory)", + ), + }, + required=[], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + return VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + directory = args.get("directory", ".") + + try: + user_groups = self._get_user_groups(tool_context) + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + args_model = self.vanna_tool.get_args_schema()(directory=directory) + result = await self.vanna_tool.execute(vanna_context, args_model) + return str(result.result_for_llm) + except Exception as e: + return f"Error listing files: {str(e)}" + + +class SearchFilesTool(BaseTool): + """Search for files by name or content.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaSearchFilesTool(file_system=file_system) + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="search_files", + description="Search for files by name or content.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "query": types.Schema( + type=types.Type.STRING, + description="Text to search for in file names or contents", + ), + "include_content": types.Schema( + type=types.Type.BOOLEAN, + description="Whether to search within file contents (default: True)", + ), + "max_results": types.Schema( + type=types.Type.INTEGER, + description="Maximum number of matches to return (default: 20)", + ), + }, + required=["query"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + return VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + query = args.get("query", "").strip() + include_content = args.get("include_content", True) + max_results = args.get("max_results", 20) + + if not query: + return "Error: No search query provided" + + try: + user_groups = self._get_user_groups(tool_context) + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + args_model = self.vanna_tool.get_args_schema()( + query=query, include_content=include_content, max_results=max_results + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + return str(result.result_for_llm) + except Exception as e: + return f"Error searching files: {str(e)}" + + +class EditFileTool(BaseTool): + """Modify specific lines within a file.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaEditFileTool(file_system=file_system) + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="edit_file", + description="Modify specific lines within a file.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "filename": types.Schema( + type=types.Type.STRING, + description="Path to the file to edit", + ), + "edits": types.Schema( + type=types.Type.ARRAY, + description="List of edits to apply", + items=types.Schema( + type=types.Type.OBJECT, + properties={ + "start_line": types.Schema( + type=types.Type.INTEGER, + description="First line (1-based) affected by this edit", + ), + "end_line": types.Schema( + type=types.Type.INTEGER, + description="Last line (1-based, inclusive) to replace", + ), + "new_content": types.Schema( + type=types.Type.STRING, + description="Replacement text", + ), + }, + ), + ), + }, + required=["filename", "edits"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + return VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + filename = args.get("filename", "").strip() + edits = args.get("edits", []) + + if not filename: + return "Error: No filename provided" + + if not edits: + return "Error: No edits provided" + + try: + user_groups = self._get_user_groups(tool_context) + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + args_model = self.vanna_tool.get_args_schema()( + filename=filename, edits=edits + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + return str(result.result_for_llm) + except Exception as e: + return f"Error editing file: {str(e)}" diff --git a/veadk/tools/vanna_tools/python.py b/veadk/tools/vanna_tools/python.py new file mode 100644 index 00000000..70926f0b --- /dev/null +++ b/veadk/tools/vanna_tools/python.py @@ -0,0 +1,216 @@ +from typing import Any, Dict, Optional, List +from google.adk.tools import BaseTool, ToolContext +from google.genai import types +from vanna.tools.python import ( + RunPythonFileTool as VannaRunPythonFileTool, + PipInstallTool as VannaPipInstallTool, +) +from vanna.core.user import User +from vanna.core.tool import ToolContext as VannaToolContext + + +class RunPythonFileTool(BaseTool): + """Execute a Python file using the workspace interpreter.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the run Python file tool with custom file_system. + + Args: + file_system: A Vanna file system instance (e.g., LocalFileSystem) + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + access_groups: List of user groups that can access this tool + """ + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaRunPythonFileTool(file_system=file_system) + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="run_python_file", + description="Execute a Python file using the workspace interpreter.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "filename": types.Schema( + type=types.Type.STRING, + description="Python file to execute (relative to the workspace root)", + ), + "arguments": types.Schema( + type=types.Type.ARRAY, + description="Optional arguments to pass to the Python script", + items=types.Schema(type=types.Type.STRING), + ), + "timeout_seconds": types.Schema( + type=types.Type.NUMBER, + description="Optional timeout for the command in seconds", + ), + }, + required=["filename"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + return VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + filename = args.get("filename", "").strip() + arguments = args.get("arguments", []) + timeout_seconds = args.get("timeout_seconds") + + if not filename: + return "Error: No filename provided" + + try: + user_groups = self._get_user_groups(tool_context) + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + args_model = self.vanna_tool.get_args_schema()( + filename=filename, arguments=arguments, timeout_seconds=timeout_seconds + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + return str(result.result_for_llm) + except Exception as e: + return f"Error running Python file: {str(e)}" + + +class PipInstallTool(BaseTool): + """Install Python packages using pip.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the pip install tool with custom file_system. + + Args: + file_system: A Vanna file system instance (e.g., LocalFileSystem) + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + access_groups: List of user groups that can access this tool (default: admin only) + """ + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaPipInstallTool(file_system=file_system) + self.access_groups = access_groups or [ + "admin" + ] # Default: only admin can install packages + + super().__init__( + name="pip_install", + description="Install Python packages using pip.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "packages": types.Schema( + type=types.Type.ARRAY, + description="Packages (with optional specifiers) to install", + items=types.Schema(type=types.Type.STRING), + ), + "upgrade": types.Schema( + type=types.Type.BOOLEAN, + description="Whether to include --upgrade in the pip invocation (default: False)", + ), + "extra_args": types.Schema( + type=types.Type.ARRAY, + description="Additional arguments to pass to pip install", + items=types.Schema(type=types.Type.STRING), + ), + "timeout_seconds": types.Schema( + type=types.Type.NUMBER, + description="Optional timeout for the command in seconds", + ), + }, + required=["packages"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + return VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + packages = args.get("packages", []) + upgrade = args.get("upgrade", False) + extra_args = args.get("extra_args", []) + timeout_seconds = args.get("timeout_seconds") + + if not packages: + return "Error: No packages provided" + + try: + user_groups = self._get_user_groups(tool_context) + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + args_model = self.vanna_tool.get_args_schema()( + packages=packages, + upgrade=upgrade, + extra_args=extra_args, + timeout_seconds=timeout_seconds, + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + return str(result.result_for_llm) + except Exception as e: + return f"Error installing packages: {str(e)}" diff --git a/veadk/tools/vanna_tools/run_sql.py b/veadk/tools/vanna_tools/run_sql.py new file mode 100644 index 00000000..2c1af2c2 --- /dev/null +++ b/veadk/tools/vanna_tools/run_sql.py @@ -0,0 +1,109 @@ +from typing import Any, Dict, Optional, List +from google.adk.tools import BaseTool, ToolContext +from google.genai import types +from vanna.tools import RunSqlTool as VannaRunSqlTool +from vanna.core.user import User +from vanna.core.tool import ToolContext as VannaToolContext + + +class RunSqlTool(BaseTool): + """Execute SQL queries against a database.""" + + def __init__( + self, + sql_runner, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the SQL tool with custom sql_runner and file_system. + + Args: + sql_runner: A Vanna SQL runner instance (e.g., SqliteRunner, PostgresRunner) + file_system: A Vanna file system instance (e.g., LocalFileSystem) + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + access_groups: List of user groups that can access this tool (e.g., ['admin', 'user']) + """ + self.sql_runner = sql_runner + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaRunSqlTool( + sql_runner=sql_runner, file_system=file_system + ) + self.access_groups = access_groups or ["admin", "user"] # Default: all groups + + super().__init__( + name="run_sql", # Keep the same name as Vanna + description="Execute a SQL query against the database and return results as a CSV file.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "sql": types.Schema( + type=types.Type.STRING, + description="The SQL query to execute", + ), + }, + required=["sql"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + """Get user groups from context.""" + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + """Check if user has access to this tool.""" + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + """Create Vanna context from Veadk ToolContext.""" + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + + vanna_context = VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + return vanna_context + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + """Execute the SQL query.""" + sql = args.get("sql", "").strip() + + if not sql: + return "Error: No SQL query provided" + + try: + # Get user groups and check access + user_groups = self._get_user_groups(tool_context) + + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + # Create Vanna context once per request + vanna_context = self._create_vanna_context(tool_context, user_groups) + + # Execute using Vanna tool + args_model = self.vanna_tool.get_args_schema()(sql=sql) + result = await self.vanna_tool.execute(vanna_context, args_model) + + return str(result.result_for_llm) + except Exception as e: + return f"Error executing SQL query: {str(e)}" diff --git a/veadk/tools/vanna_tools/summarize_data.py b/veadk/tools/vanna_tools/summarize_data.py new file mode 100644 index 00000000..44d2a814 --- /dev/null +++ b/veadk/tools/vanna_tools/summarize_data.py @@ -0,0 +1,110 @@ +import pandas as pd +import io +from typing import Any, Dict, Optional, List +from google.adk.tools import BaseTool, ToolContext +from google.genai import types +from vanna.core.user import User +from vanna.core.tool import ToolContext as VannaToolContext + + +class SummarizeDataTool(BaseTool): + """Generate statistical summaries of CSV data files.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the summarize data tool with custom file_system. + + Args: + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + file_system: A Vanna file system instance (e.g., LocalFileSystem) + access_groups: List of user groups that can access this tool (e.g., ['admin', 'user']) + """ + self.agent_memory = agent_memory + self.file_system = file_system + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="summarize_data", + description="Generate a statistical summary of data from a CSV file.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "filename": types.Schema( + type=types.Type.STRING, + description="The name of the CSV file to summarize", + ), + }, + required=["filename"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + """Get user groups from context.""" + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + """Check if user has access to this tool.""" + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + """Create Vanna context from Veadk ToolContext.""" + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + + vanna_context = VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + return vanna_context + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + """Generate a statistical summary of CSV data.""" + filename = args.get("filename", "").strip() + + if not filename: + return "Error: No filename provided" + + try: + user_groups = self._get_user_groups(tool_context) + + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + + # Read the file content + content = await self.file_system.read_file(filename, vanna_context) + + # Parse into DataFrame + df = pd.read_csv(io.StringIO(content)) + + # Generate summary stats + description = df.describe().to_markdown() + head = df.head().to_markdown() + info = f"Rows: {len(df)}, Columns: {len(df.columns)}\nColumn Names: {', '.join(df.columns)}" + + summary = f"**Data Summary for {filename}**\n\n**Info:**\n{info}\n\n**First 5 Rows:**\n{head}\n\n**Statistical Description:**\n{description}" + return summary + except Exception as e: + return f"Failed to summarize data: {str(e)}" diff --git a/veadk/tools/vanna_tools/visualize_data.py b/veadk/tools/vanna_tools/visualize_data.py new file mode 100644 index 00000000..0716c03b --- /dev/null +++ b/veadk/tools/vanna_tools/visualize_data.py @@ -0,0 +1,109 @@ +from typing import Any, Dict, Optional, List +from google.adk.tools import BaseTool, ToolContext +from google.genai import types +from vanna.tools import VisualizeDataTool as VannaVisualizeDataTool +from vanna.core.user import User +from vanna.core.tool import ToolContext as VannaToolContext + + +class VisualizeDataTool(BaseTool): + """Visualize data from CSV files.""" + + def __init__( + self, + file_system, + agent_memory, + access_groups: Optional[List[str]] = None, + ): + """ + Initialize the visualization tool with custom file_system. + + Args: + file_system: A Vanna file system instance (e.g., LocalFileSystem) + agent_memory: A Vanna agent memory instance (e.g., DemoAgentMemory) + access_groups: List of user groups that can access this tool (e.g., ['admin', 'user']) + user_group_resolver: Optional callable that takes ToolContext and returns user groups + """ + self.file_system = file_system + self.agent_memory = agent_memory + self.vanna_tool = VannaVisualizeDataTool(file_system=file_system) + self.access_groups = access_groups or ["admin", "user"] + + super().__init__( + name="visualize_data", # Keep the same name as Vanna + description="Create visualizations from CSV data files.", + ) + + def _get_declaration(self) -> types.FunctionDeclaration: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "filename": types.Schema( + type=types.Type.STRING, + description="The name of the CSV file to visualize", + ), + "title": types.Schema( + type=types.Type.STRING, + description="Optional title for the chart", + ), + }, + required=["filename"], + ), + ) + + def _get_user_groups(self, tool_context: ToolContext) -> List[str]: + """Get user groups from context.""" + user_groups = tool_context.state.get("user_groups", ["user"]) + return user_groups + + def _check_access(self, user_groups: List[str]) -> bool: + """Check if user has access to this tool.""" + return any(group in self.access_groups for group in user_groups) + + def _create_vanna_context( + self, tool_context: ToolContext, user_groups: List[str] + ) -> VannaToolContext: + """Create Vanna context from Veadk ToolContext.""" + user_id = tool_context.user_id + user_email = tool_context.state.get("user_email", "user@example.com") + + vanna_user = User(id=user_id, email=user_email, group_memberships=user_groups) + + vanna_context = VannaToolContext( + user=vanna_user, + conversation_id=tool_context.session.id, + request_id=tool_context.session.id, + agent_memory=self.agent_memory, + ) + + return vanna_context + + async def run_async( + self, *, args: Dict[str, Any], tool_context: ToolContext + ) -> str: + """Create a visualization from CSV data.""" + filename = args.get("filename", "").strip() + title = args.get("title") + + if not filename: + return "Error: No filename provided" + + try: + user_groups = self._get_user_groups(tool_context) + + if not self._check_access(user_groups): + return f"Error: Access denied. This tool requires one of the following groups: {', '.join(self.access_groups)}" + + vanna_context = self._create_vanna_context(tool_context, user_groups) + + args_model = self.vanna_tool.get_args_schema()( + filename=filename, title=title + ) + result = await self.vanna_tool.execute(vanna_context, args_model) + + return str(result.result_for_llm) + except Exception as e: + return f"Error visualizing data: {str(e)}" From 62bb088bf68374be73890f08d51fade548732966 Mon Sep 17 00:00:00 2001 From: "wuqingfu.528" Date: Mon, 9 Feb 2026 16:20:02 +0800 Subject: [PATCH 2/6] fix: add license header --- veadk/tools/vanna_tools/agent_memory.py | 14 ++++++++++++++ veadk/tools/vanna_tools/file_system.py | 14 ++++++++++++++ veadk/tools/vanna_tools/python.py | 14 ++++++++++++++ veadk/tools/vanna_tools/run_sql.py | 14 ++++++++++++++ veadk/tools/vanna_tools/summarize_data.py | 14 ++++++++++++++ veadk/tools/vanna_tools/visualize_data.py | 14 ++++++++++++++ 6 files changed, 84 insertions(+) diff --git a/veadk/tools/vanna_tools/agent_memory.py b/veadk/tools/vanna_tools/agent_memory.py index 9c02502e..f4034ef4 100644 --- a/veadk/tools/vanna_tools/agent_memory.py +++ b/veadk/tools/vanna_tools/agent_memory.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Any, Dict, Optional, List from google.adk.tools import BaseTool, ToolContext from google.genai import types diff --git a/veadk/tools/vanna_tools/file_system.py b/veadk/tools/vanna_tools/file_system.py index d5cd8939..432fb112 100644 --- a/veadk/tools/vanna_tools/file_system.py +++ b/veadk/tools/vanna_tools/file_system.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Any, Dict, Optional, List from google.adk.tools import BaseTool, ToolContext from google.genai import types diff --git a/veadk/tools/vanna_tools/python.py b/veadk/tools/vanna_tools/python.py index 70926f0b..c2d3ba5a 100644 --- a/veadk/tools/vanna_tools/python.py +++ b/veadk/tools/vanna_tools/python.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Any, Dict, Optional, List from google.adk.tools import BaseTool, ToolContext from google.genai import types diff --git a/veadk/tools/vanna_tools/run_sql.py b/veadk/tools/vanna_tools/run_sql.py index 2c1af2c2..0a083423 100644 --- a/veadk/tools/vanna_tools/run_sql.py +++ b/veadk/tools/vanna_tools/run_sql.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Any, Dict, Optional, List from google.adk.tools import BaseTool, ToolContext from google.genai import types diff --git a/veadk/tools/vanna_tools/summarize_data.py b/veadk/tools/vanna_tools/summarize_data.py index 44d2a814..1a5a407a 100644 --- a/veadk/tools/vanna_tools/summarize_data.py +++ b/veadk/tools/vanna_tools/summarize_data.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import pandas as pd import io from typing import Any, Dict, Optional, List diff --git a/veadk/tools/vanna_tools/visualize_data.py b/veadk/tools/vanna_tools/visualize_data.py index 0716c03b..4a0fee8c 100644 --- a/veadk/tools/vanna_tools/visualize_data.py +++ b/veadk/tools/vanna_tools/visualize_data.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Any, Dict, Optional, List from google.adk.tools import BaseTool, ToolContext from google.genai import types From ab06d6a16280064841585f7b3dc28cfddb141470 Mon Sep 17 00:00:00 2001 From: "wuqingfu.528" Date: Mon, 9 Feb 2026 16:22:05 +0800 Subject: [PATCH 3/6] fix: add license header --- veadk/tools/vanna_tools/examples/agent.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/veadk/tools/vanna_tools/examples/agent.py b/veadk/tools/vanna_tools/examples/agent.py index a770eafd..917b17bf 100644 --- a/veadk/tools/vanna_tools/examples/agent.py +++ b/veadk/tools/vanna_tools/examples/agent.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os from veadk import Agent, Runner @@ -38,6 +52,7 @@ def setup_sqlite(): return db_path +# Create a session with user groups for access control async def create_session(user_groups: list = ["user"]): session_service = InMemorySessionService() example_session = await session_service.create_session( From 755a9e165f5498d2aefac37e58447ea88334eb76 Mon Sep 17 00:00:00 2001 From: haoxingjun Date: Tue, 10 Feb 2026 12:25:28 +0800 Subject: [PATCH 4/6] feat: add veadk-vanna-proj example with B2B data agent --- examples/veadk-vanna-proj/__init__.py | 13 + examples/veadk-vanna-proj/clean.py | 23 ++ examples/veadk-vanna-proj/config.yaml.example | 6 + examples/veadk-vanna-proj/deploy.py | 106 +++++++ examples/veadk-vanna-proj/src/.adk/session.db | Bin 0 -> 36864 bytes examples/veadk-vanna-proj/src/__init__.py | 13 + examples/veadk-vanna-proj/src/agent.py | 26 ++ examples/veadk-vanna-proj/src/app.py | 171 +++++++++++ .../src/data_agent/.adk/session.db | Bin 0 -> 802816 bytes .../src/data_agent/__init__.py | 18 ++ .../veadk-vanna-proj/src/data_agent/agent.py | 168 +++++++++++ .../veadk-vanna-proj/src/data_agent/tools.py | 277 ++++++++++++++++++ .../veadk-vanna-proj/src/requirements.txt | 5 + examples/veadk-vanna-proj/src/run.sh | 49 ++++ .../src/sample_data/README.md | 134 +++++++++ .../src/sample_data/b2b_crm.sqlite | Bin 0 -> 32768 bytes .../src/sample_data/b2b_data_gen.py | 124 ++++++++ 17 files changed, 1133 insertions(+) create mode 100644 examples/veadk-vanna-proj/__init__.py create mode 100644 examples/veadk-vanna-proj/clean.py create mode 100644 examples/veadk-vanna-proj/config.yaml.example create mode 100644 examples/veadk-vanna-proj/deploy.py create mode 100644 examples/veadk-vanna-proj/src/.adk/session.db create mode 100644 examples/veadk-vanna-proj/src/__init__.py create mode 100644 examples/veadk-vanna-proj/src/agent.py create mode 100644 examples/veadk-vanna-proj/src/app.py create mode 100644 examples/veadk-vanna-proj/src/data_agent/.adk/session.db create mode 100644 examples/veadk-vanna-proj/src/data_agent/__init__.py create mode 100644 examples/veadk-vanna-proj/src/data_agent/agent.py create mode 100644 examples/veadk-vanna-proj/src/data_agent/tools.py create mode 100644 examples/veadk-vanna-proj/src/requirements.txt create mode 100755 examples/veadk-vanna-proj/src/run.sh create mode 100644 examples/veadk-vanna-proj/src/sample_data/README.md create mode 100644 examples/veadk-vanna-proj/src/sample_data/b2b_crm.sqlite create mode 100644 examples/veadk-vanna-proj/src/sample_data/b2b_data_gen.py diff --git a/examples/veadk-vanna-proj/__init__.py b/examples/veadk-vanna-proj/__init__.py new file mode 100644 index 00000000..7f463206 --- /dev/null +++ b/examples/veadk-vanna-proj/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/veadk-vanna-proj/clean.py b/examples/veadk-vanna-proj/clean.py new file mode 100644 index 00000000..f3f195f3 --- /dev/null +++ b/examples/veadk-vanna-proj/clean.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from veadk.cloud.cloud_app import CloudApp + +def main() -> None: + cloud_app = CloudApp(vefaas_application_name="veadk-cloud-vanna-agent") + cloud_app.delete_self() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/veadk-vanna-proj/config.yaml.example b/examples/veadk-vanna-proj/config.yaml.example new file mode 100644 index 00000000..7f2fdc08 --- /dev/null +++ b/examples/veadk-vanna-proj/config.yaml.example @@ -0,0 +1,6 @@ +model: + agent: + provider: openai + name: doubao-1-5-pro-256k-250115 + api_base: https://ark.cn-beijing.volces.com/api/v3/ + api_key: diff --git a/examples/veadk-vanna-proj/deploy.py b/examples/veadk-vanna-proj/deploy.py new file mode 100644 index 00000000..77468d92 --- /dev/null +++ b/examples/veadk-vanna-proj/deploy.py @@ -0,0 +1,106 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from pathlib import Path + +from a2a.types import TextPart +from fastmcp.client import Client + +from veadk.cloud.cloud_agent_engine import CloudAgentEngine +from veadk.cloud.cloud_app import CloudApp, get_message_id + +SESSION_ID = "cloud_app_test_session" +USER_ID = "cloud_app_test_user" + + +async def _send_msg_with_a2a(cloud_app: CloudApp, message: str) -> None: + print("===== A2A example =====") + + response_message = await cloud_app.message_send(message, SESSION_ID, USER_ID) + + if not response_message or not response_message.parts: + print( + "No response from VeFaaS application. Something wrong with cloud application." + ) + return + + print(f"Message ID: {get_message_id(response_message)}") + + if isinstance(response_message.parts[0].root, TextPart): + print( + f"Response from {cloud_app.vefaas_endpoint}: {response_message.parts[0].root.text}" + ) + else: + print( + f"Response from {cloud_app.vefaas_endpoint}: {response_message.parts[0].root}" + ) + + +async def _send_msg_with_mcp(cloud_app: CloudApp, message: str) -> None: + print("===== MCP example =====") + + endpoint = cloud_app._get_vefaas_endpoint() + print(f"MCP server endpoint: {endpoint}/mcp") + + # Connect to MCP server + client = Client(f"{endpoint}/mcp") + + async with client: + # List available tools + tools = await client.list_tools() + print(f"Available tools: {tools}") + + # Call run_agent tool, pass user input and session information + res = await client.call_tool( + "run_agent", + { + "user_input": message, + "session_id": SESSION_ID, + "user_id": USER_ID, + }, + ) + print(f"Response from {cloud_app.vefaas_endpoint}: {res}") + + +async def main(): + engine = CloudAgentEngine() + + cloud_app = engine.deploy( + path=str(Path(__file__).parent / "src"), + application_name="veadk-cloud-vanna-agent", + gateway_name="dong-mcp-agent2", + gateway_service_name="", + gateway_upstream_name="", + use_adk_web=True, + auth_method="none", + identity_user_pool_name="", + identity_client_name="", + local_test=False, # Set to True for local testing before deploy to VeFaaS + ) + print(f"VeFaaS application ID: {cloud_app.vefaas_application_id}") + + if False: + print(f"Web is running at: {cloud_app.vefaas_endpoint}") + else: + # Test with deployed cloud application + message = "How is the weather like in Beijing?" + print(f"Test message: {message}") + + # await _send_msg_with_a2a(cloud_app=cloud_app, message=message) + # await _send_msg_with_mcp(cloud_app=cloud_app, message=message) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/veadk-vanna-proj/src/.adk/session.db b/examples/veadk-vanna-proj/src/.adk/session.db new file mode 100644 index 0000000000000000000000000000000000000000..dd390d0e75efd92470c5c618d9cf0e087a765b1a GIT binary patch literal 36864 zcmeI)&rj1}7{Kuswsj0}s4>kBBrop5A|T_3%#+Sh2yucAh;nMWtt5-=hqMqA;{iy# z`44&V;2&VT`Zsv-BsYEA(QKFwB^(s;eY3Xj`mWpae4egJ+idI4iZ$03+s)m&<%(Hl zL{U}cp%99qMCCLfr=Ue-$D4wt2AwmOQDy4=@o@53i4Ufg~x>bqgrD(~3#zTI%0_B>uF8Tk!EY~&vm4bfgF z#uGxeT2*WqFE+&Lnw&O^#R-3{)oPgyt8Ryl?K$?Y8NQigJ5H_H=rLMr>^Ccx+xrD> zt!_K6Rc{FyZt=H}{HV>Uh)GG^BSSzh1~M8i~xEcuaqgQk7bxYQJ_~ z?Z|T()7o>J{&TZk5Hs6e4jlGHHGOncJ$mjJ@+5R8SjJ8jGgy04&LH(LqcaNEqnxfK z&$&W&kIJqsM@+9?VWrw@$ujjG{!^`_VIp%k7Sju(y~Kj7&1}%q4odx+o=&TWFMTyX zS-CU0w$s&(oU-d0xEP=RIbl#2l8_4Xk(j=c?xoNbznSfN5r|)+?7z1ZK9=@0SPDxOYGu_GTmAjD`RL2q1s} z0tg_000IagfB*tlRUqLd#`*tMEiZW?fB*srAb-rBsbY0hrN`;Wr8-1hx@*%^DXJX4;ToSWaCp3KeXs*}~q z!opm6x{|BrtT%7-U#-nZD~WVlb;p&pdo+&0tg_000IagfB*srAb`N-5SWh2 zYXSr3xFW#s|1U>ji4p+>5I_I{1Q0*~0R#|00D+4L@caLZ=&>yV2q1s}0tg_000Iag KfB*uQL*N&sf3A`M literal 0 HcmV?d00001 diff --git a/examples/veadk-vanna-proj/src/__init__.py b/examples/veadk-vanna-proj/src/__init__.py new file mode 100644 index 00000000..7f463206 --- /dev/null +++ b/examples/veadk-vanna-proj/src/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/veadk-vanna-proj/src/agent.py b/examples/veadk-vanna-proj/src/agent.py new file mode 100644 index 00000000..ab5948e3 --- /dev/null +++ b/examples/veadk-vanna-proj/src/agent.py @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from data_agent.agent import agent # type: ignore + +from veadk.memory.short_term_memory import ShortTermMemory +from veadk.types import AgentRunConfig + +# [required] instantiate the agent run configuration +agent_run_config = AgentRunConfig( + app_name="vanna_sql_agent", + agent=agent, # type: ignore + short_term_memory=ShortTermMemory(backend="local", local_database_path="/tmp/session.db"), # type: ignore + model_extra_config={"extra_body": {"thinking": {"type": "disabled"}}} +) diff --git a/examples/veadk-vanna-proj/src/app.py b/examples/veadk-vanna-proj/src/app.py new file mode 100644 index 00000000..af0340ce --- /dev/null +++ b/examples/veadk-vanna-proj/src/app.py @@ -0,0 +1,171 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from contextlib import asynccontextmanager +from typing import Callable + +from agent import agent_run_config + +from fastapi import FastAPI +from fastapi.routing import APIRoute + +from fastmcp import FastMCP + +from starlette.routing import Route + +from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder +from a2a.types import AgentProvider + +from veadk.a2a.ve_a2a_server import init_app +from veadk.runner import Runner +from veadk.types import AgentRunConfig +from veadk.utils.logger import get_logger +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +from opentelemetry import context + +logger = get_logger(__name__) + +assert isinstance(agent_run_config, AgentRunConfig), ( + f"Invalid agent_run_config type: {type(agent_run_config)}, expected `AgentRunConfig`" +) + +app_name = agent_run_config.app_name +agent = agent_run_config.agent +short_term_memory = agent_run_config.short_term_memory + +VEFAAS_REGION = os.getenv("APP_REGION", "cn-beijing") +VEFAAS_FUNC_ID = os.getenv("_FAAS_FUNC_ID", "") +agent_card_builder = AgentCardBuilder(agent=agent, provider=AgentProvider(organization="Volcengine Agent Development Kit (VeADK)", url=f"https://console.volcengine.com/vefaas/region:vefaas+{VEFAAS_REGION}/function/detail/{VEFAAS_FUNC_ID}")) + + +def build_mcp_run_agent_func() -> Callable: + runner = Runner( + agent=agent, + short_term_memory=short_term_memory, + app_name=app_name, + user_id="", + ) + + async def run_agent( + user_input: str, + user_id: str = "mcp_user", + session_id: str = "mcp_session", + ) -> str: + # Set user_id for runner + runner.user_id = user_id + + # Running agent and get final output + final_output = await runner.run( + messages=user_input, + session_id=session_id, + ) + return final_output + + run_agent_doc = f"""{agent.description} + Args: + user_input: User's input message (required). + user_id: User identifier. Defaults to "mcp_user". + session_id: Session identifier. Defaults to "mcp_session". + Returns: + Final agent response as a string.""" + + run_agent.__doc__ = run_agent_doc + + return run_agent + + +async def agent_card() -> dict: + agent_card = await agent_card_builder.build() + return agent_card.model_dump() + +async def get_cozeloop_space_id() -> dict: + return {"space_id": os.getenv("OBSERVABILITY_OPENTELEMETRY_COZELOOP_SERVICE_NAME", default="")} + +# Build a run_agent function for building MCP server +run_agent_func = build_mcp_run_agent_func() + +a2a_app = init_app( + server_url="0.0.0.0", + app_name=app_name, + agent=agent, + short_term_memory=short_term_memory, +) + +a2a_app.post("/run_agent", operation_id="run_agent", tags=["mcp"])(run_agent_func) +a2a_app.get("/agent_card", operation_id="agent_card", tags=["mcp"])(agent_card) +a2a_app.get("/get_cozeloop_space_id", operation_id="get_cozeloop_space_id", tags=["mcp"])(get_cozeloop_space_id) + +# === Build mcp server === + +mcp = FastMCP.from_fastapi(app=a2a_app, name=app_name, include_tags={"mcp"}) + +# Create MCP ASGI app +mcp_app = mcp.http_app(path="/", transport="streamable-http") + + +# Combined lifespan management +@asynccontextmanager +async def combined_lifespan(app: FastAPI): + async with mcp_app.lifespan(app): + yield + + +# Create main FastAPI app with combined lifespan +app = FastAPI( + title=a2a_app.title, + version=a2a_app.version, + lifespan=combined_lifespan, + openapi_url=None, + docs_url=None, + redoc_url=None +) + +@app.middleware("http") +async def otel_context_middleware(request, call_next): + carrier = { + "traceparent": request.headers.get("Traceparent"), + "tracestate": request.headers.get("Tracestate"), + } + logger.debug(f"carrier: {carrier}") + if carrier["traceparent"] is None: + return await call_next(request) + else: + ctx = TraceContextTextMapPropagator().extract(carrier=carrier) + logger.debug(f"ctx: {ctx}") + token = context.attach(ctx) + try: + response = await call_next(request) + finally: + context.detach(token) + return response + +# Mount A2A routes to main app +for route in a2a_app.routes: + app.routes.append(route) + +# Mount MCP server at /mcp endpoint +app.mount("/mcp", mcp_app) + + +# remove openapi routes +paths = ["/openapi.json", "/docs", "/redoc"] +new_routes = [] +for route in app.router.routes: + if isinstance(route, (APIRoute, Route)) and route.path in paths: + continue + new_routes.append(route) +app.router.routes = new_routes + +# === Build mcp server end === diff --git a/examples/veadk-vanna-proj/src/data_agent/.adk/session.db b/examples/veadk-vanna-proj/src/data_agent/.adk/session.db new file mode 100644 index 0000000000000000000000000000000000000000..6fb1f62f6180b9ef369b44545595cb3f0e9c760b GIT binary patch literal 802816 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ⅅ%bWi9y{^&#dle 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�nqmTeK*mitHeOkP}d6hpLFZ_}LR|XcAcR!ILa% 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 literal 0 HcmV?d00001 diff --git a/examples/veadk-vanna-proj/src/data_agent/__init__.py b/examples/veadk-vanna-proj/src/data_agent/__init__.py new file mode 100644 index 00000000..a2539111 --- /dev/null +++ b/examples/veadk-vanna-proj/src/data_agent/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .agent import agent + +# required from Google ADK Web +root_agent = agent diff --git a/examples/veadk-vanna-proj/src/data_agent/agent.py b/examples/veadk-vanna-proj/src/data_agent/agent.py new file mode 100644 index 00000000..f83a479b --- /dev/null +++ b/examples/veadk-vanna-proj/src/data_agent/agent.py @@ -0,0 +1,168 @@ +from veadk import Agent +from google.adk.planners import PlanReActPlanner +from .tools import ( + run_sql, + visualize_data, + save_correctanswer_memory, + search_similar_tools, + generate_document, + summarize_data, + run_python_file, + pip_install, + read_file, + edit_file, + list_files, + search_files, + save_text_memory +) + +# Define the Veadk Agent using Vanna Tools +agent: Agent = Agent( + name="b2b_data_agent", + description="Assistant for querying B2B customer, revenue, and usage data.", + instruction=""" + You are a data analysis agent for a Cloud Service Provider. + You have access to a SQLite database `b2b_crm.sqlite` with the following schema: + + - `customer`: Stores customer profiles. Key fields: `name` (full name), `short_name`, `is_main_customer` (1=True), `sales_team`. + - `revenue`: Monthly revenue data. Fields: `year_month` (YYYY-MM), `product_name`, `amount`. + - `resource_usage`: Daily usage data. Fields: `usage_date` (YYYY-MM-DD), `resource_type` (Tokens, GPU), `quantity`. + - `account_credit`: Credit status. Fields: `total_credit_limit`, `available_balance`, `arrears_amount` (positive means debt). + + **Available Tools:** + - `run_sql(sql)`: Executes SQL queries on the B2B CRM database. + - `run_python_file(filename)`: Executes Python scripts. + - `pip_install(packages)`: Installs Python packages. + - `visualize_data(filename, title)`: Creates visualizations from CSV files generated by SQL queries. + - `summarize_data(filename)`: Generates statistical summaries of CSV files. + - `generate_document(filename, content)`: Creates a new file with the given content. + - `read_file(filename, start_line, end_line)`: Reads the content of a file. + - `edit_file(filename, edits)`: Edits a file by replacing lines. + - `list_files(path)`: Lists files in a directory. + - `search_files(query, path)`: Searches for files matching a query. + - `search_similar_tools(question, limit)`: Searches for similar past tool usages. + - `save_correctanswer_memory(question, tool_name, args)`: Saves successful tool usages. + - `save_text_memory(text, tags)`: Saves arbitrary text to memory for future retrieval. + + **Strategy for Ambiguous Requests:** + 1. **Name Disambiguation**: If a user asks for "Xiaomi" (小米), ALWAYS check `customer` table first. Prefer `is_main_customer=1` unless specified otherwise. + - *Example SQL*: `SELECT * FROM customer WHERE (name LIKE '%小米%' OR short_name = '小米') AND is_main_customer = 1` + 2. **Time Ranges**: + - "Last 3 months" usually means the last 3 completed billing cycles in `revenue` table. + - "Recent trend" implies querying `resource_usage` and plotting the `quantity` over `usage_date`. + 3. **Missing Data**: If specific daily data (e.g., "today") is requested but not in the DB, explain that data might not be generated yet. + + **Report Generation Requirement:** + For complex analysis tasks (e.g., "Analyze anomaly", "Generate report", "Forecast trend", "Diagnose issue"), you MUST: + 1. Perform the analysis using SQL and Python. + 2. **Generate a Markdown Report**: Use `generate_document` to save a detailed report (e.g., `analysis_report.md`). + - The report MUST include: **Executive Summary**, **Methodology** (SQL/Python logic), **Detailed Findings** (with data tables/charts), and **Recommendations**. + 3. In your Final Answer, provide a brief summary AND the file path of the generated report. + + **Output Requirement:** + You MUST describe the detailed execution process in your final answer using **Chinese**. The description should include: + 1. **Thought Process**: How you analyzed the request and what strategy you chose. + 2. **Tool Usage**: Which tools were used, with what parameters (e.g., specific SQL queries). + 3. **Intermediate Results**: Key findings from each step (e.g., "Found customer ID ACC-001 for Xiaomi"). + 4. **Final Answer**: The direct answer to the user's question, supported by the data found. + + **Planning Strategy:** + 1. **Analyze the Request**: Determine if the request requires simple database retrieval (Text-to-SQL) or complex analysis/calculation (Text-to-Python). + 2. **Formulate a Plan**: + * **Simple Path (Text-to-SQL)**: If the request is a direct data lookup (e.g., "What were the sales last month?"), create a plan to write and execute a SQL query using `run_sql`. + * **Complex Path (Text-to-Python/Multi-turn)**: If the request involves advanced analytics, predictions, complex logic, or non-SQL operations (e.g., "Predict next month's sales trend", "Find anomalies in sales"), create a multi-step plan: + a. Retrieve necessary data using `run_sql`. + b. Write a Python script using `generate_document` to process the data. + c. Execute the script using `run_python_file`. + d. Analyze the output. + 3. **Execute & Observe**: Follow your plan, executing tools and observing outputs. + 4. **Iterative Refinement (Multi-turn)**: + * **Error Recovery**: If a tool execution fails (e.g., SQL syntax error, Python runtime error), analyze the error message, revise your plan, and retry using `REPLANNING`. + * **Clarification**: If the request is ambiguous, ask the user for clarification. + + Here is the schema details of the B2B CRM database: + ```sql + CREATE TABLE IF NOT EXISTS customer ( + customer_id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- Full Name + short_name TEXT, -- Short Name + is_main_customer BOOLEAN, -- Is Main Customer (1=Yes, 0=No) + customer_level TEXT, -- Customer Level (Strategic, KA, NA) + owner TEXT, -- Owner Name + sales_team TEXT, -- Sales Team + industry TEXT, + status TEXT + ); + CREATE TABLE IF NOT EXISTS revenue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id TEXT, + year_month TEXT, -- Revenue Month (YYYY-MM) + product_category TEXT, -- Product Category + product_name TEXT, -- Product Name + amount REAL, -- Revenue Amount + FOREIGN KEY(customer_id) REFERENCES customer(customer_id) + ); + CREATE TABLE IF NOT EXISTS resource_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id TEXT, + usage_date TEXT, -- Usage Date (YYYY-MM-DD) + resource_type TEXT, -- Resource Type + model_or_card TEXT, -- Model/Card Type + quantity REAL, -- Usage Quantity + FOREIGN KEY(customer_id) REFERENCES customer(customer_id) + ); + CREATE TABLE IF NOT EXISTS account_credit ( + customer_id TEXT PRIMARY KEY, + total_credit_limit REAL, -- Total Credit Limit + available_balance REAL, -- Available Balance + arrears_amount REAL, -- Arrears Amount + FOREIGN KEY(customer_id) REFERENCES customer(customer_id) + ); + ``` + + Here are some examples of how to query this database: + + Q: "小米客户近3个月的收入" (Ambiguous Name & Time Range) + Thought: User asks for "Xiaomi". I need to find the main customer "Xiaomi" to avoid "Xiaomi Shoes". "Last 3 months" refers to revenue data. + Plan: + 1. Find the `customer_id` for "Xiaomi" where `is_main_customer=1`. + 2. Query `revenue` table for this `customer_id` for the last 3 months. + A: SELECT sum(amount) FROM revenue WHERE customer_id = (SELECT customer_id FROM customer WHERE (name LIKE '%小米%' OR short_name LIKE '%小米%') AND is_main_customer=1) AND year_month >= strftime('%Y-%m', date('now', '-3 months')) + + Q: "小米最近的用量趋势" (Complex Trend Visualization) + Thought: User wants "trend". This requires daily data from `resource_usage` and a chart. + Plan: + 1. Get `customer_id` for "Xiaomi" (Main Customer). + 2. Query `usage_date` and `quantity` from `resource_usage` for the last 30 days. + 3. Save result to CSV. + 4. Call `visualize_data` to plot the trend. + A: (Plan to use `run_sql` then `visualize_data`) + + Q: "查一下分期乐的信控情况,有没有欠费?" (Derived Metric & Join) + Thought: "Debt" or "Arrears" means checking `arrears_amount` in `account_credit` table. + Plan: + 1. Find `customer_id` for "Fenqile" (分期乐). + 2. Join `customer` and `account_credit` to get credit limit, balance, and arrears. + 3. If `arrears_amount` > 0, report it as debt. + A: SELECT c.name, a.total_credit_limit, a.available_balance, a.arrears_amount FROM customer c JOIN account_credit a ON c.customer_id = a.customer_id WHERE c.name LIKE '%分期乐%' OR c.short_name LIKE '%分期乐%' + + 1. Use `run_sql` to execute queries. + """, + tools=[ + run_sql, # RunSqlTool: Execute SQL queries + visualize_data, # VisualizeDataTool: Create visualizations + save_correctanswer_memory, # SaveQuestionToolArgsTool: Save tool usage examples + search_similar_tools, # SearchSavedCorrectToolUsesTool: Search tool usage examples + generate_document, # WriteFileTool: Create new files + summarize_data, # SummarizeDataTool: Summarize CSV data + run_python_file, # RunPythonFileTool: Execute Python scripts + pip_install, # PipInstallTool: Install Python packages + read_file, # ReadFileTool: Read file content + edit_file, # EditFileTool: Edit file content + list_files, # ListFilesTool: List directory content + search_files, # SearchFilesTool: Search for files + save_text_memory # SaveTextMemoryTool: Save text to memory + ], + planner=PlanReActPlanner(), + model_extra_config={"extra_body": {"thinking": {"type": "disabled"}}} +) diff --git a/examples/veadk-vanna-proj/src/data_agent/tools.py b/examples/veadk-vanna-proj/src/data_agent/tools.py new file mode 100644 index 00000000..377ea510 --- /dev/null +++ b/examples/veadk-vanna-proj/src/data_agent/tools.py @@ -0,0 +1,277 @@ +import os +import httpx +import asyncio +import pandas as pd +import io +from typing import Optional, Dict, Any + +from vanna.integrations.sqlite import SqliteRunner +from vanna.tools.file_system import ( + LocalFileSystem, + WriteFileTool, + ReadFileTool, + EditFileTool, + ListFilesTool, + SearchFilesTool +) +from vanna.tools import ( + RunSqlTool, + VisualizeDataTool +) +from vanna.tools.python import RunPythonFileTool, PipInstallTool +from vanna.tools.agent_memory import ( + SaveQuestionToolArgsTool, + SearchSavedCorrectToolUsesTool, + SaveTextMemoryTool +) +from vanna.integrations.local.agent_memory import DemoAgentMemory +from vanna.core.tool import ToolContext +from vanna.core.registry import ToolRegistry +from vanna.core.user import User + +# Setup SQLite +def setup_sqlite(): + # Use the generated B2B sample data + # Note: In VeFaaS, only /tmp is writable, so we might need to copy it there if we want to modify it. + # But for read-only access or local dev, we can point to the sample_data directory. + + # Try to find the sample data relative to this file + current_dir = os.path.dirname(os.path.abspath(__file__)) + # Go up one level to src, then to sample_data + sample_data_path = os.path.join(os.path.dirname(current_dir), 'sample_data', 'b2b_crm.sqlite') + + if os.path.exists(sample_data_path): + return sample_data_path + + # Fallback to Chinook if B2B data not found (or for compatibility) + db_path = "/tmp/Chinook.sqlite" + if not os.path.exists(db_path): + print("Downloading Chinook.sqlite...") + url = "https://vanna.ai/Chinook.sqlite" + try: + with open(db_path, "wb") as f: + with httpx.stream("GET", url) as response: + for chunk in response.iter_bytes(): + f.write(chunk) + except Exception as e: + print(f"Error downloading database: {e}") + return db_path + +# Initialize Resources +db_path = setup_sqlite() +# Use /tmp for file storage as it's the only writable directory in VeFaaS +file_system = LocalFileSystem(working_directory="/tmp/data_storage") +if not os.path.exists("/tmp/data_storage"): + os.makedirs("/tmp/data_storage", exist_ok=True) + +sqlite_runner = SqliteRunner(database_path=db_path) +agent_memory = DemoAgentMemory(max_items=1000) + +# Initialize Vanna Tools +sql_tool = RunSqlTool(sql_runner=sqlite_runner, file_system=file_system) +viz_tool = VisualizeDataTool(file_system=file_system) +run_python_tool = RunPythonFileTool(file_system=file_system) +pip_install_tool = PipInstallTool(file_system=file_system) + +# File System Tools +write_file_tool = WriteFileTool(file_system=file_system) +read_file_tool = ReadFileTool(file_system=file_system) +edit_file_tool = EditFileTool(file_system=file_system) +list_files_tool = ListFilesTool(file_system=file_system) +search_files_tool = SearchFilesTool(file_system=file_system) + +save_mem_tool = SaveQuestionToolArgsTool() +search_mem_tool = SearchSavedCorrectToolUsesTool() +save_text_mem_tool = SaveTextMemoryTool() + +# Create a mock context for tool execution +# In a real application, this should be created per-request with the actual user +mock_user = User(id="veadk-user", email="user@example.com", group_memberships=["admin", "user"]) +mock_context = ToolContext( + user=mock_user, + conversation_id="default", + request_id="default", + agent_memory=agent_memory +) + +# Wrapper Functions for Veadk Agent + +async def run_sql(sql: str) -> str: + """ + Execute a SQL query against the Chinook database. + + Args: + sql: The SQL query to execute. + """ + args_model = sql_tool.get_args_schema()(sql=sql) + result = await sql_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def visualize_data(filename: str, title: str = None) -> str: + """ + Visualize data from a CSV file. + + Args: + filename: The name of the CSV file to visualize. + title: Optional title for the chart. + """ + # Check if the file is likely a CSV file + if not filename.lower().endswith('.csv'): + return f"Error: visualize_data only supports CSV files. You provided: {filename}" + + args_model = viz_tool.get_args_schema()(filename=filename, title=title) + result = await viz_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def run_python_file(filename: str) -> str: + """ + Execute a Python file. + + Args: + filename: The name of the Python file to execute. + """ + # Check if the file is likely a Python file + if not filename.lower().endswith('.py'): + return f"Error: run_python_file only supports Python files. You provided: {filename}" + + args_model = run_python_tool.get_args_schema()(filename=filename) + result = await run_python_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def pip_install(packages: list[str]) -> str: + """ + Install Python packages using pip. + + Args: + packages: List of package names to install. + """ + args_model = pip_install_tool.get_args_schema()(packages=packages) + result = await pip_install_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def read_file(filename: str, start_line: int = 1, end_line: int = -1) -> str: + """ + Read the content of a file. + + Args: + filename: The name of the file to read. + start_line: The line number to start reading from (1-based). + end_line: The line number to stop reading at (inclusive). -1 for end of file. + """ + args_model = read_file_tool.get_args_schema()(filename=filename, start_line=start_line, end_line=end_line) + result = await read_file_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def edit_file(filename: str, edits: list[dict[str, Any]]) -> str: + """ + Edit a file by replacing lines. + + Args: + filename: The name of the file to edit. + edits: A list of edits to apply. Each edit is a dictionary with: + - start_line: The line number to start replacing (1-based). + - end_line: The line number to stop replacing (inclusive). + - new_content: The new content to insert. + """ + # Convert dicts to EditFileTool.Edit objects if necessary, but Pydantic should handle dicts + args_model = edit_file_tool.get_args_schema()(filename=filename, edits=edits) + result = await edit_file_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def list_files(path: str = ".") -> str: + """ + List files in a directory. + + Args: + path: The directory path to list. Defaults to current directory. + """ + args_model = list_files_tool.get_args_schema()(path=path) + result = await list_files_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def search_files(query: str, path: str = ".") -> str: + """ + Search for files matching a query. + + Args: + query: The search query (regex or glob pattern). + path: The directory path to search in. Defaults to current directory. + """ + args_model = search_files_tool.get_args_schema()(query=query, path=path) + result = await search_files_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def save_correctanswer_memory(question: str, tool_name: str, args: Dict[str, Any]) -> str: + """ + Save a successful question-tool-argument combination for future reference. + + Args: + question: The original question that was asked. + tool_name: The name of the tool that was used successfully. + args: The arguments that were passed to the tool. + """ + # Temporarily disabled due to infinite loop issues + return "Memory saved successfully (Simulated)" + +async def search_similar_tools(question: str, limit: int = 10) -> str: + """ + Search for similar tool usage patterns based on a question. + + Args: + question: The question to find similar tool usage patterns for. + limit: Maximum number of results to return. + """ + args_model = search_mem_tool.get_args_schema()(question=question, limit=limit) + result = await search_mem_tool.execute(mock_context, args_model) + # Return the result (whether success or error message) + return str(result.result_for_llm) + +async def save_text_memory(text: str, tags: list[str] = None) -> str: + """ + Save arbitrary text to memory for future retrieval. + + Args: + text: The text content to save. + tags: Optional list of tags to categorize the memory. + """ + # Note: SaveTextMemoryParams uses 'content' field, but we expose it as 'text' to the LLM for clarity. + # We map 'text' to 'content' here. 'tags' are not currently supported by SaveTextMemoryParams in this version of Vanna. + args_model = save_text_mem_tool.get_args_schema()(content=text) + result = await save_text_mem_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def generate_document(filename: str, content: str) -> str: + """ + Generate a document (save content to a file). + + Args: + filename: The name of the file to save (e.g., 'report.md', 'summary.txt'). + content: The text content to write to the file. + """ + args_model = write_file_tool.get_args_schema()(filename=filename, content=content, overwrite=True) + result = await write_file_tool.execute(mock_context, args_model) + return str(result.result_for_llm) + +async def summarize_data(filename: str) -> str: + """ + Generate a statistical summary of data from a CSV file. + + Args: + filename: The name of the CSV file to summarize. + """ + try: + # Read the file content + content = await file_system.read_file(filename, mock_context) + + # Parse into DataFrame + df = pd.read_csv(io.StringIO(content)) + + # Generate summary stats + description = df.describe().to_markdown() + head = df.head().to_markdown() + info = f"Rows: {len(df)}, Columns: {len(df.columns)}\nColumn Names: {', '.join(df.columns)}" + + summary = f"**Data Summary for {filename}**\n\n**Info:**\n{info}\n\n**First 5 Rows:**\n{head}\n\n**Statistical Description:**\n{description}" + return summary + except Exception as e: + return f"Failed to summarize data: {str(e)}" diff --git a/examples/veadk-vanna-proj/src/requirements.txt b/examples/veadk-vanna-proj/src/requirements.txt new file mode 100644 index 00000000..d2858f5c --- /dev/null +++ b/examples/veadk-vanna-proj/src/requirements.txt @@ -0,0 +1,5 @@ +veadk-python==0.5.16 +fastapi==0.123.10 +uvicorn[standard]==0.40.0 +vanna==2.0.1 +httpx==0.28.1 diff --git a/examples/veadk-vanna-proj/src/run.sh b/examples/veadk-vanna-proj/src/run.sh new file mode 100755 index 00000000..b0f80137 --- /dev/null +++ b/examples/veadk-vanna-proj/src/run.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -ex +cd `dirname $0` + +# A special check for CLI users (run.sh should be located at the 'root' dir) +if [ -d "output" ]; then + cd ./output/ +fi + +# Default values for host and port +HOST="0.0.0.0" +PORT=${_FAAS_RUNTIME_PORT:-8000} +TIMEOUT=${_FAAS_FUNC_TIMEOUT} + +export SERVER_HOST=$HOST +export SERVER_PORT=$PORT + +export PYTHONPATH=$PYTHONPATH:./site-packages + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --port) + PORT="$2" + shift 2 + ;; + --host) + HOST="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + + +USE_ADK_WEB=${USE_ADK_WEB:-False} + +export SHORT_TERM_MEMORY_BACKEND= # can be `mysql` +export LONG_TERM_MEMORY_BACKEND= # can be `opensearch` + +if [ "$USE_ADK_WEB" = "True" ]; then + echo "USE_ADK_WEB is True, running veadk web" + exec python3 -m veadk.cli.cli web --host $HOST +else + echo "USE_ADK_WEB is False, running A2A and MCP server" + exec python3 -m uvicorn app:app --host $HOST --port $PORT --timeout-graceful-shutdown $TIMEOUT --loop asyncio +fi diff --git a/examples/veadk-vanna-proj/src/sample_data/README.md b/examples/veadk-vanna-proj/src/sample_data/README.md new file mode 100644 index 00000000..9648d9ea --- /dev/null +++ b/examples/veadk-vanna-proj/src/sample_data/README.md @@ -0,0 +1,134 @@ +# B2B Data Agent 样例问题集 + +本文档列出了10个典型的 B2B 业务场景问题,对比了使用本 Agent 前后的效果差异。为了展示 Agent 的高级分析能力,我们特别设计了多个需要结合 SQL 和 Python 脚本解决的复杂问题(Text-to-Python)。 + +## 场景 1: 客户名称模糊匹配 + +### 1. 小米客户近3个月的收入 + +**Bad Case (原逻辑):** +可能直接模糊匹配 `name LIKE '%小米%'`,导致将“宁波小米粒鞋业”的收入也计算在内,或者因为存在多个匹配项而报错。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 识别到“小米”可能存在歧义,优先查找 `customer` 表中 `is_main_customer=1` 的记录。 +2. **Action**: `run_sql("SELECT customer_id FROM customer WHERE (name LIKE '%小米%' OR short_name = '小米') AND is_main_customer = 1")` -> 找到 ACC-001。 +3. **Action**: `run_sql("SELECT sum(amount) FROM revenue WHERE customer_id = 'ACC-001' AND year_month >= ...")` +4. **Result**: 准确返回小米科技(主客户)的收入数据,排除干扰项。 + +### 2. 网易2025年的总收入 + +**Bad Case (原逻辑):** +可能无法区分“网易”是指集团还是某个子公司,或者漏掉某些月份的数据。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 查找“网易”对应的主客户 ID。 +2. **Action**: 锁定 ACC-005 (网易(杭州)网络有限公司)。 +3. **Action**: 聚合查询 2025 全年的 `revenue`。 +4. **Result**: 返回网易 2025 年度的准确总收入。 + +## 场景 2: 复杂趋势与预测 (SQL + Python) + +### 3. 小米最近的用量趋势(含可视化) + +**Bad Case (原逻辑):** +仅返回一堆数字或 SQL 查询结果,用户无法直观理解趋势。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 用户需要“趋势”,意味着需要日粒度数据并生成图表。 +2. **Action**: `run_sql` 查询 `resource_usage` 表获取近 30 天的 `usage_date` 和 `quantity`,保存为 CSV。 +3. **Action**: 调用 `visualize_data` 工具,传入 CSV 数据生成折线图。 +4. **Result**: 返回一张清晰的用量趋势折线图。 + +### 4. 预测小米下个月的用量增长 + +**Bad Case (原逻辑):** +无法处理“预测”请求,因为 SQL 无法进行时间序列预测。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 这是一个预测任务,需要提取历史数据并使用 Python 进行线性回归或简单外推。 +2. **Action**: `run_sql` 导出小米过去 3 个月的用量数据到 CSV。 +3. **Action**: `generate_document` 编写 Python 脚本:读取 CSV,使用 `scikit-learn` 或 `numpy` 拟合趋势线,预测下月总量。 +4. **Action**: `run_python_file` 执行预测脚本。 +5. **Result**: "基于过去3个月的增长趋势,预测小米下个月的用量约为 35,000,000 Tokens,环比增长 5%。" + +## 场景 3: 复杂逻辑与异常检测 (SQL + Python) + +### 5. 帮我分析一下小米的云资源使用异常 + +**Bad Case (原逻辑):** +不知道什么是“异常”,直接报错或返回空。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 这是一个复杂分析任务。需要定义异常(如 3-Sigma 准则或 IQR)。 +2. **Action**: `run_sql` 获取小米的历史日用量数据。 +3. **Action**: `generate_document` 编写 Python 脚本:计算均值和标准差,识别超过 `mean + 3*std` 的日期。 +4. **Action**: `run_python_file` 执行脚本。 +5. **Action**: `generate_document` 生成分析报告 `xiaomi_anomaly_analysis.md`。 +6. **Result**: "检测到 15天前 用量激增,达到 3,000,000,超过平均值 3 倍,属于异常波动。详细分析请见报告:`xiaomi_anomaly_analysis.md`" + +### 6. 计算网易 DeepSeek 调用的周环比增长率 + +**Bad Case (原逻辑):** +SQL 计算周环比(WoW)非常复杂,容易出错。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 使用 Python pandas 处理时间序列更高效。 +2. **Action**: `run_sql` 获取网易 DeepSeek 的每日用量数据。 +3. **Action**: `generate_document` 编写 Python 脚本:`df.resample('W').sum().pct_change()` 计算周环比。 +4. **Action**: `run_python_file` 执行脚本。 +5. **Result**: "上周对比上上周,DeepSeek 调用量增长了 15.2%。" + +## 场景 4: 跨表关联与业务洞察 + +### 7. 哪些欠费客户还在大量使用资源?(风险预警) + +**Bad Case (原逻辑):** +无法同时关联欠费状态和近期用量,或者 SQL 逻辑过于复杂导致超时。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 需要找到 `arrears_amount > 0` 的客户,并检查其最近 3 天的 `resource_usage` 是否超过阈值。 +2. **Action**: `run_sql` 联查 `customer`, `account_credit`, `resource_usage`,筛选欠费且近3天有用量的客户。 +3. **Result**: "警告:分期乐(ACC-004)当前欠费 74.8万元,但最近3天仍消耗了 500 GPU Hours,建议立即介入。" + +### 8. 谁是 DeepSeek 模型最大的使用方? + +**Bad Case (原逻辑):** +无法将“DeepSeek”映射到 `model_or_card` 字段,或者不知道如何定义“最大使用方”。 + +**Expected (Agent 逻辑):** + +1. **Thought**: “DeepSeek” 对应 `resource_usage` 表中的 `model_or_card`。 +2. **Action**: 按 `customer_id` 分组统计 `quantity` 总和,按降序排列,取第一名。 +3. **Result**: "网易(ACC-005)是 DeepSeek 模型的最大使用方,累计消耗 Token 超过 6000 万。" + +## 场景 5: 综合报告生成 (SQL + Python) + +### 9. 生成一份小米的月度消费报告 + +**Bad Case (原逻辑):** +只能返回零散的数据,无法生成结构化报告。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 报告需要包含收入、用量、趋势图和关键指标。这是一个多步任务。 +2. **Action 1**: `run_sql` 查询当月收入和用量总和。 +3. **Action 2**: `run_sql` 查询日用量趋势,并调用 `visualize_data` 生成图表。 +4. **Action 3**: `generate_document` 将上述数据和图表路径整合成 Markdown 格式的报告 `xiaomi_monthly_report.md`,报告中必须包含各步骤的详细分析。 +5. **Result**: "报告已生成:`xiaomi_monthly_report.md`。摘要:小米本月总消费 xxx 元,趋势平稳,无异常波动。" + +### 10. 今天的最新收入数据出来了吗? + +**Bad Case (原逻辑):** +尝试查询数据库,返回空结果,用户不知道是没数据还是没发生交易。 + +**Expected (Agent 逻辑):** + +1. **Thought**: 命中 Instruction 中的 "Missing Data" 策略。 +2. **Result**: 直接回答:“根据数据库设计,`revenue` 表按月更新,`resource_usage` 按日更新。今天的实时收入数据尚未生成,通常需要在次月出账后查看。” diff --git a/examples/veadk-vanna-proj/src/sample_data/b2b_crm.sqlite b/examples/veadk-vanna-proj/src/sample_data/b2b_crm.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..bca835da1c2cd6131f3d55a0630c117fe6f07f87 GIT binary patch literal 32768 zcmeI4eQXow9mnlBeu*9Xl295_FU|25e4GuolkhUOHYOOFk+;SPEwq|*I7ecU*ug%t z(oIuZUK*esVK6}{FCnZ2X*7hvMo2(oZ(XI)iD}iet=g7pv6HlafYdQn+dq5GzUR(o z?)uMYwW+_8g3ou~=X1|}p679L_uvaW-4Nzja$7Xk%W!0srbwgJYHCSRqtWQmj|u(Y zB_FNuKX}pNl}~xqYnB~cakF`7SQ9?HseUhh;*cXd=P|Lov29%*o#w>stN3Hb2?sTif6#<@-p7nG}{# zBF;s7*;p{#Nw)c)Z6jM+>l=Noo5_v-%}z1JMLDKNI83l7+#BXdL~n4Cq{~I7`X{D- zayIpy^RvUJlH+fQg_xa8xQDN1a4XZpL_#bs!xu^(x-@!q3#5-ptuY!( zn<=d(9O-0Vi0|k@fA$5L1Q$)e56YzlJ@WUa-7WfbW9)k_-54=89!)%; zgRC*+`?4AXwLyJTn}379HM&NZtlM_(!nIG**jSo&=o=BcPNIKf0 z+u2CmNp5Iq4?cxXKQ2*vqn&I|Fd7Snm{_K7;d=0mdEqQ7zdZRG|3W&}v20&}Ww$%Y z2CrK}>_{*XF3j!9?g^PQkV!_RlZ_-$??}Jo%NWAO&!9i-LZd3!8;x+?3jJYb z^-i*` zCz|Mdur%*OD#^E@RC@5W>GPwcqml0~WPM~CvV(M|`;-KF@FO9sOQZTH_rCdXKN@yF zsWq0CmuvSuoo+Zh*zjwftm*L9thU3O@aW7Q(b7m2y^u}KZDdn>1OJ(tUXY*U-V4Z# z*tg@|(HIvLOW+_%^P^#;KRX+a2YZ=tB#3K4u5E5^@cWva=}*$*my&~TO^rdi*@;2yijJKZHY0a-+{lHuThG&nT5lf&CXY@-8_e61XoMX5| zJPk>2qn76xO6$tiKHSBV_24xf`lBL!ZEr;@dYe;23=?0o-M77pR$&1HU;qq&0Wbgt zzyKHk17H9QfPpW#fv@Y$_V%)Zic))dnXj(S<#yLh-#nZ;e&p_rfz+wDlh;n)y|Eu{ znVvYAI=O%L^}*!ci^)S{nT&W99Y{phG8=s^KNIKbBODuxu$(W%g?F-D`DQy)QBZ7O zVi)+;scV;$C$A*OUPkv_&^^4V>w_8o^!dZ71O3@Y5d#~2Te_KOLpZTM9BE@i-L4I> zXrd3j;cv@k4iwOt?R6EVIb{`P!hx!iBm0vhFYyN&Jv4p!N^0!f?71`i{hsk-;+8<; z+Gm(Z*E5Mgqu&*XCb;famP7Y_Gtfkzosd;J zeK52Op&P=1Fc0RvWOJX_n(g(-dU-jv?h%Mt4)_}l6B8NVnZ2TKWFyiQU?NC)6S}F= zEq884eOgcaQbXJ)W{E!&zeTICfB`T72EYIq00UqE41fVJ00zJS7ytwRiv}z@yH>a% zqnFmJ^QHAFowV-FGwbXHnY@Re|1Ys_(Gc$wBgD(Z^Mso)+fufl*-qPDv~99^Z8qzN z)_1JK)^DQ_EMNc(fB`T72EYIq00UqE41fVJpkQD@iBV&>7nkdVD>JX#TjTP0ef9VY zAjY%gf?}nNN1iEC%DCkjLLuW`Ezj7LGOOent5RmAJX5HYSs~9@lrlB)j9DpDEzcAv zWvb*ElTs$fvr#D{_q@o!XG)8WnfCGu?UnwL$EL>GM>9>lRG%i%`NH+H*s3{S>CjxW zYZm1z5k0v?oe~k%O^KMNM0Dp8wMs>zu-w!imDc!cgI=LW^b6a>X)WdCuat=*)jG|G?aD z4wxMU|1P*waIWBmf{g{F>0{HorZ-G|raIF+<449@#v{gV;~Ha;;qQhq!$Cubp~_&= zf1n@L@76!7ck4~43b23yFaQP~s)0%$x)(aXVoqM>er+|nU%SHP_PX4w#K$Pf0vg4pK{%xE@y(4hv6RbG>tmrj)o@QHk5!PdQY$Ipf%+>!X*b zaB)=PI{K(eWv(}896M`Cda*KBj7nT>2en9riwCFRf&SJVdZ7vzM@4S&pIWE|DqK7` z1@6-8{q%elE{;mvE437PsqIJU9ieBRWXiRk%1Rai1un9#i4s!6|T;T&$(bRJb@QaZ5GSJQXe;oC3FU<{n+D z!o^XMTXgpx^{5IrXS`J|9-`-}aB)=PzB@@hqQb?4NT~1Ptu6E%6)uiS+_QHmem4Gr z-^U)8Cuh7ZI-{ZaN%{F@Ilqsyq7wJTTNFP#uf)ZJNT~0^4@PNzl3t06qY^hTO7XMx zN?bgMFn3-D%}?4ZadA}QI)^BJ_FjpL2a&*C&|XCIllV$p9F@5GL5iQvSK{J9Byi^+ zzD4ts`bu0J6}iM;`YC>PUx|wck-#N?(?|1@{7PILmAKdXD1NqIiHipj=2}RapYm7Y z;;6(O?4bBLeR literal 0 HcmV?d00001 diff --git a/examples/veadk-vanna-proj/src/sample_data/b2b_data_gen.py b/examples/veadk-vanna-proj/src/sample_data/b2b_data_gen.py new file mode 100644 index 00000000..f818cf6b --- /dev/null +++ b/examples/veadk-vanna-proj/src/sample_data/b2b_data_gen.py @@ -0,0 +1,124 @@ +import sqlite3 +import random +from datetime import datetime, timedelta +import os + +def create_b2b_database(): + db_path = os.path.join(os.path.dirname(__file__), 'b2b_crm.sqlite') + # If db exists, remove it to start fresh + if os.path.exists(db_path): + os.remove(db_path) + + conn = sqlite3.connect(db_path) + c = conn.cursor() + + # 1. 客户表:解决名称歧义 (小米 vs 小米粒)、归属关系 (Owner/SalesTeam) + c.execute('''CREATE TABLE IF NOT EXISTS customer ( + customer_id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- 全称 + short_name TEXT, -- 简称 + is_main_customer BOOLEAN, -- 是否主客户 + customer_level TEXT, -- 客户等级 (Strategic, KA, NA) + owner TEXT, -- 负责人 + sales_team TEXT, -- 销售团队 + industry TEXT, + status TEXT + )''') + + # 2. 收入表:解决 "最近3个月收入"、"分产品收入" + c.execute('''CREATE TABLE IF NOT EXISTS revenue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id TEXT, + year_month TEXT, -- 计收月份 (YYYY-MM) + product_category TEXT, -- 产品分类 (AI, Cloud) + product_name TEXT, -- 产品名称 (Model Inference, GPU) + amount REAL, -- 收入金额 + FOREIGN KEY(customer_id) REFERENCES customer(customer_id) + )''') + + # 3. 用量表:解决 "最近3天调用量"、"Tokens趋势" (日粒度) + c.execute('''CREATE TABLE IF NOT EXISTS resource_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id TEXT, + usage_date TEXT, -- 用量日期 (YYYY-MM-DD) + resource_type TEXT, -- 资源类型 (Tokens, GPU_Hours) + model_or_card TEXT, -- 模型/卡型 (DeepSeek, L20) + quantity REAL, -- 用量数值 + FOREIGN KEY(customer_id) REFERENCES customer(customer_id) + )''') + + # 4. 信控表:解决 "信控额度"、"欠费" + c.execute('''CREATE TABLE IF NOT EXISTS account_credit ( + customer_id TEXT PRIMARY KEY, + total_credit_limit REAL, -- 总信控额度 + available_balance REAL, -- 可用余额 + arrears_amount REAL, -- 欠费金额 + FOREIGN KEY(customer_id) REFERENCES customer(customer_id) + )''') + + # --- 注入针对 Bad Case 的数据 --- + + # Case A: 模糊匹配 & 主客户逻辑 ("小米") + customers = [ + ('ACC-001', '小米科技有限责任公司', '小米', 1, 'Strategic', 'ZhangSan', 'KA-North', 'Internet', 'Active'), + ('ACC-002', '宁波小米粒鞋业有限公司', '小米粒', 0, 'SMB', 'LiSi', 'SME-East', 'Retail', 'Active'), + ('ACC-003', '安宁市小米渣食品店', '小米渣', 0, 'SMB', 'WangWu', 'SME-South', 'Retail', 'Active'), + ('ACC-004', '深圳市分期乐网络科技有限公司', '分期乐', 1, 'KA', 'ZhaoLiu', 'FinTech-Group', 'Finance', 'Active'), + ('ACC-005', '网易(杭州)网络有限公司', '网易', 1, 'Strategic', 'SunBa', 'KA-East', 'Internet', 'Active'), + ] + c.executemany('INSERT OR REPLACE INTO customer VALUES (?,?,?,?,?,?,?,?,?)', customers) + + # Case B: 复杂时间窗口收入 ("小米最近3个月收入") + # 假设当前是 2026-01,生成 2025-10 ~ 2025-12 的数据 + revenue_data = [] + for month in ['2025-10', '2025-11', '2025-12']: + # 小米 (ACC-001) - 只有主客户有大额收入 + revenue_data.append(('ACC-001', month, 'AI', 'Model Inference', 12000000.00)) + # 小米粒 (ACC-002) - 极小金额 + revenue_data.append(('ACC-002', month, 'Cloud', 'VM', 5.50)) + + # Case C: 分产品收入 ("网易 25年 Deepseek 收入") + for m in range(1, 13): + m_str = f"2025-{m:02d}" + revenue_data.append(('ACC-005', m_str, 'AI', 'DeepSeek', 500000.00)) + + c.executemany('INSERT INTO revenue (customer_id, year_month, product_category, product_name, amount) VALUES (?,?,?,?,?)', revenue_data) + + # Case D: 用量趋势 ("联想/小米 近3天调用量") + usage_data = [] + # 使用当前日期作为基准,确保"最近3天"总是有数据 + today = datetime.now() + base_date = today - timedelta(days=30) + + for i in range(31): # 生成过去30天到今天的数据 + d = (base_date + timedelta(days=i)).strftime('%Y-%m-%d') + + # 小米 (ACC-001) 每天调用量波动 (Doubao-Pro) + # 正常波动: 100w + i*1w + # 异常点注入: 15天前 (i=15) 突然飙升到 300w (是均值的约2-3倍) + quantity = 1000000 + i*10000 + if i == 15: + quantity = 3000000 # 异常点 + + usage_data.append(('ACC-001', d, 'Tokens', 'Doubao-Pro', quantity)) + + # 网易 (ACC-005) 使用 DeepSeek 模型 + usage_data.append(('ACC-005', d, 'Tokens', 'DeepSeek', 2000000 + i*50000)) + + # Case E: 信控与欠费 ("分期乐信控余额" & "欠费仍在使用") + # 分期乐 (ACC-004): 欠费 74w + c.execute("INSERT OR REPLACE INTO account_credit VALUES ('ACC-004', 400000, -348787.45, 748787.45)") + + # 分期乐最近3天仍有 GPU 用量 (模拟风险场景) + for i in range(3): + d = (today - timedelta(days=i)).strftime('%Y-%m-%d') + usage_data.append(('ACC-004', d, 'GPU_Hours', 'L20', 500)) + + c.executemany('INSERT INTO resource_usage (customer_id, usage_date, resource_type, model_or_card, quantity) VALUES (?,?,?,?,?)', usage_data) + + conn.commit() + conn.close() + print(f"B2B 模拟数据库已生成: {db_path}") + +if __name__ == "__main__": + create_b2b_database() From 63d660d25aabb18f2dd98fad2805e4d62b25167b Mon Sep 17 00:00:00 2001 From: "wuqingfu.528" Date: Tue, 10 Feb 2026 20:21:14 +0800 Subject: [PATCH 5/6] fix: c360 --- .../veadk-vanna-proj/src/data_agent/agent.py | 426 ++++++++++++------ .../veadk-vanna-proj/src/data_agent/tools.py | 304 +++++++++++-- 2 files changed, 539 insertions(+), 191 deletions(-) diff --git a/examples/veadk-vanna-proj/src/data_agent/agent.py b/examples/veadk-vanna-proj/src/data_agent/agent.py index f83a479b..c739b348 100644 --- a/examples/veadk-vanna-proj/src/data_agent/agent.py +++ b/examples/veadk-vanna-proj/src/data_agent/agent.py @@ -1,168 +1,296 @@ from veadk import Agent from google.adk.planners import PlanReActPlanner from .tools import ( - run_sql, - visualize_data, - save_correctanswer_memory, - search_similar_tools, - generate_document, - summarize_data, + run_sql, + visualize_data, + save_correctanswer_memory, + search_similar_tools, + generate_document, + summarize_data, run_python_file, pip_install, read_file, edit_file, list_files, search_files, - save_text_memory + save_text_memory, + query_with_dsl, + recall_metadata, ) +# # Define the Veadk Agent using Vanna Tools +# agent: Agent = Agent( +# name="b2b_data_agent", +# description="Assistant for querying B2B customer, revenue, and usage data.", +# instruction=""" +# You are a data analysis agent for a Cloud Service Provider. +# You have access to a SQLite database `b2b_crm.sqlite` with the following schema: + +# - `customer`: Stores customer profiles. Key fields: `name` (full name), `short_name`, `is_main_customer` (1=True), `sales_team`. +# - `revenue`: Monthly revenue data. Fields: `year_month` (YYYY-MM), `product_name`, `amount`. +# - `resource_usage`: Daily usage data. Fields: `usage_date` (YYYY-MM-DD), `resource_type` (Tokens, GPU), `quantity`. +# - `account_credit`: Credit status. Fields: `total_credit_limit`, `available_balance`, `arrears_amount` (positive means debt). + +# **Available Tools:** +# - `run_sql(sql)`: Executes SQL queries on the B2B CRM database. +# - `run_python_file(filename)`: Executes Python scripts. +# - `pip_install(packages)`: Installs Python packages. +# - `visualize_data(filename, title)`: Creates visualizations from CSV files generated by SQL queries. +# - `summarize_data(filename)`: Generates statistical summaries of CSV files. +# - `generate_document(filename, content)`: Creates a new file with the given content. +# - `read_file(filename, start_line, end_line)`: Reads the content of a file. +# - `edit_file(filename, edits)`: Edits a file by replacing lines. +# - `list_files(path)`: Lists files in a directory. +# - `search_files(query, path)`: Searches for files matching a query. +# - `search_similar_tools(question, limit)`: Searches for similar past tool usages. +# - `save_correctanswer_memory(question, tool_name, args)`: Saves successful tool usages. +# - `save_text_memory(text, tags)`: Saves arbitrary text to memory for future retrieval. + +# **Strategy for Ambiguous Requests:** +# 1. **Name Disambiguation**: If a user asks for "Xiaomi" (小米), ALWAYS check `customer` table first. Prefer `is_main_customer=1` unless specified otherwise. +# - *Example SQL*: `SELECT * FROM customer WHERE (name LIKE '%小米%' OR short_name = '小米') AND is_main_customer = 1` +# 2. **Time Ranges**: +# - "Last 3 months" usually means the last 3 completed billing cycles in `revenue` table. +# - "Recent trend" implies querying `resource_usage` and plotting the `quantity` over `usage_date`. +# 3. **Missing Data**: If specific daily data (e.g., "today") is requested but not in the DB, explain that data might not be generated yet. + +# **Report Generation Requirement:** +# For complex analysis tasks (e.g., "Analyze anomaly", "Generate report", "Forecast trend", "Diagnose issue"), you MUST: +# 1. Perform the analysis using SQL and Python. +# 2. **Generate a Markdown Report**: Use `generate_document` to save a detailed report (e.g., `analysis_report.md`). +# - The report MUST include: **Executive Summary**, **Methodology** (SQL/Python logic), **Detailed Findings** (with data tables/charts), and **Recommendations**. +# 3. In your Final Answer, provide a brief summary AND the file path of the generated report. + +# **Output Requirement:** +# You MUST describe the detailed execution process in your final answer using **Chinese**. The description should include: +# 1. **Thought Process**: How you analyzed the request and what strategy you chose. +# 2. **Tool Usage**: Which tools were used, with what parameters (e.g., specific SQL queries). +# 3. **Intermediate Results**: Key findings from each step (e.g., "Found customer ID ACC-001 for Xiaomi"). +# 4. **Final Answer**: The direct answer to the user's question, supported by the data found. + +# **Planning Strategy:** +# 1. **Analyze the Request**: Determine if the request requires simple database retrieval (Text-to-SQL) or complex analysis/calculation (Text-to-Python). +# 2. **Formulate a Plan**: +# * **Simple Path (Text-to-SQL)**: If the request is a direct data lookup (e.g., "What were the sales last month?"), create a plan to write and execute a SQL query using `run_sql`. +# * **Complex Path (Text-to-Python/Multi-turn)**: If the request involves advanced analytics, predictions, complex logic, or non-SQL operations (e.g., "Predict next month's sales trend", "Find anomalies in sales"), create a multi-step plan: +# a. Retrieve necessary data using `run_sql`. +# b. Write a Python script using `generate_document` to process the data. +# c. Execute the script using `run_python_file`. +# d. Analyze the output. +# 3. **Execute & Observe**: Follow your plan, executing tools and observing outputs. +# 4. **Iterative Refinement (Multi-turn)**: +# * **Error Recovery**: If a tool execution fails (e.g., SQL syntax error, Python runtime error), analyze the error message, revise your plan, and retry using `REPLANNING`. +# * **Clarification**: If the request is ambiguous, ask the user for clarification. + +# Here is the schema details of the B2B CRM database: +# ```sql +# CREATE TABLE IF NOT EXISTS customer ( +# customer_id TEXT PRIMARY KEY, +# name TEXT NOT NULL, -- Full Name +# short_name TEXT, -- Short Name +# is_main_customer BOOLEAN, -- Is Main Customer (1=Yes, 0=No) +# customer_level TEXT, -- Customer Level (Strategic, KA, NA) +# owner TEXT, -- Owner Name +# sales_team TEXT, -- Sales Team +# industry TEXT, +# status TEXT +# ); +# CREATE TABLE IF NOT EXISTS revenue ( +# id INTEGER PRIMARY KEY AUTOINCREMENT, +# customer_id TEXT, +# year_month TEXT, -- Revenue Month (YYYY-MM) +# product_category TEXT, -- Product Category +# product_name TEXT, -- Product Name +# amount REAL, -- Revenue Amount +# FOREIGN KEY(customer_id) REFERENCES customer(customer_id) +# ); +# CREATE TABLE IF NOT EXISTS resource_usage ( +# id INTEGER PRIMARY KEY AUTOINCREMENT, +# customer_id TEXT, +# usage_date TEXT, -- Usage Date (YYYY-MM-DD) +# resource_type TEXT, -- Resource Type +# model_or_card TEXT, -- Model/Card Type +# quantity REAL, -- Usage Quantity +# FOREIGN KEY(customer_id) REFERENCES customer(customer_id) +# ); +# CREATE TABLE IF NOT EXISTS account_credit ( +# customer_id TEXT PRIMARY KEY, +# total_credit_limit REAL, -- Total Credit Limit +# available_balance REAL, -- Available Balance +# arrears_amount REAL, -- Arrears Amount +# FOREIGN KEY(customer_id) REFERENCES customer(customer_id) +# ); +# ``` + +# Here are some examples of how to query this database: + +# Q: "小米客户近3个月的收入" (Ambiguous Name & Time Range) +# Thought: User asks for "Xiaomi". I need to find the main customer "Xiaomi" to avoid "Xiaomi Shoes". "Last 3 months" refers to revenue data. +# Plan: +# 1. Find the `customer_id` for "Xiaomi" where `is_main_customer=1`. +# 2. Query `revenue` table for this `customer_id` for the last 3 months. +# A: SELECT sum(amount) FROM revenue WHERE customer_id = (SELECT customer_id FROM customer WHERE (name LIKE '%小米%' OR short_name LIKE '%小米%') AND is_main_customer=1) AND year_month >= strftime('%Y-%m', date('now', '-3 months')) + +# Q: "小米最近的用量趋势" (Complex Trend Visualization) +# Thought: User wants "trend". This requires daily data from `resource_usage` and a chart. +# Plan: +# 1. Get `customer_id` for "Xiaomi" (Main Customer). +# 2. Query `usage_date` and `quantity` from `resource_usage` for the last 30 days. +# 3. Save result to CSV. +# 4. Call `visualize_data` to plot the trend. +# A: (Plan to use `run_sql` then `visualize_data`) + +# Q: "查一下分期乐的信控情况,有没有欠费?" (Derived Metric & Join) +# Thought: "Debt" or "Arrears" means checking `arrears_amount` in `account_credit` table. +# Plan: +# 1. Find `customer_id` for "Fenqile" (分期乐). +# 2. Join `customer` and `account_credit` to get credit limit, balance, and arrears. +# 3. If `arrears_amount` > 0, report it as debt. +# A: SELECT c.name, a.total_credit_limit, a.available_balance, a.arrears_amount FROM customer c JOIN account_credit a ON c.customer_id = a.customer_id WHERE c.name LIKE '%分期乐%' OR c.short_name LIKE '%分期乐%' + +# 1. Use `run_sql` to execute queries. +# """, +# tools=[ +# run_sql, # RunSqlTool: Execute SQL queries +# visualize_data, # VisualizeDataTool: Create visualizations +# save_correctanswer_memory, # SaveQuestionToolArgsTool: Save tool usage examples +# search_similar_tools, # SearchSavedCorrectToolUsesTool: Search tool usage examples +# generate_document, # WriteFileTool: Create new files +# summarize_data, # SummarizeDataTool: Summarize CSV data +# run_python_file, # RunPythonFileTool: Execute Python scripts +# pip_install, # PipInstallTool: Install Python packages +# read_file, # ReadFileTool: Read file content +# edit_file, # EditFileTool: Edit file content +# list_files, # ListFilesTool: List directory content +# search_files, # SearchFilesTool: Search for files +# save_text_memory # SaveTextMemoryTool: Save text to memory +# ], +# planner=PlanReActPlanner(), +# model_extra_config={"extra_body": {"thinking": {"type": "disabled"}}} +# ) + + # Define the Veadk Agent using Vanna Tools agent: Agent = Agent( name="b2b_data_agent", description="Assistant for querying B2B customer, revenue, and usage data.", instruction=""" - You are a data analysis agent for a Cloud Service Provider. - You have access to a SQLite database `b2b_crm.sqlite` with the following schema: - - - `customer`: Stores customer profiles. Key fields: `name` (full name), `short_name`, `is_main_customer` (1=True), `sales_team`. - - `revenue`: Monthly revenue data. Fields: `year_month` (YYYY-MM), `product_name`, `amount`. - - `resource_usage`: Daily usage data. Fields: `usage_date` (YYYY-MM-DD), `resource_type` (Tokens, GPU), `quantity`. - - `account_credit`: Credit status. Fields: `total_credit_limit`, `available_balance`, `arrears_amount` (positive means debt). - - **Available Tools:** - - `run_sql(sql)`: Executes SQL queries on the B2B CRM database. - - `run_python_file(filename)`: Executes Python scripts. - - `pip_install(packages)`: Installs Python packages. - - `visualize_data(filename, title)`: Creates visualizations from CSV files generated by SQL queries. - - `summarize_data(filename)`: Generates statistical summaries of CSV files. - - `generate_document(filename, content)`: Creates a new file with the given content. - - `read_file(filename, start_line, end_line)`: Reads the content of a file. - - `edit_file(filename, edits)`: Edits a file by replacing lines. - - `list_files(path)`: Lists files in a directory. - - `search_files(query, path)`: Searches for files matching a query. - - `search_similar_tools(question, limit)`: Searches for similar past tool usages. - - `save_correctanswer_memory(question, tool_name, args)`: Saves successful tool usages. - - `save_text_memory(text, tags)`: Saves arbitrary text to memory for future retrieval. - - **Strategy for Ambiguous Requests:** - 1. **Name Disambiguation**: If a user asks for "Xiaomi" (小米), ALWAYS check `customer` table first. Prefer `is_main_customer=1` unless specified otherwise. - - *Example SQL*: `SELECT * FROM customer WHERE (name LIKE '%小米%' OR short_name = '小米') AND is_main_customer = 1` - 2. **Time Ranges**: - - "Last 3 months" usually means the last 3 completed billing cycles in `revenue` table. - - "Recent trend" implies querying `resource_usage` and plotting the `quantity` over `usage_date`. - 3. **Missing Data**: If specific daily data (e.g., "today") is requested but not in the DB, explain that data might not be generated yet. - - **Report Generation Requirement:** - For complex analysis tasks (e.g., "Analyze anomaly", "Generate report", "Forecast trend", "Diagnose issue"), you MUST: - 1. Perform the analysis using SQL and Python. - 2. **Generate a Markdown Report**: Use `generate_document` to save a detailed report (e.g., `analysis_report.md`). - - The report MUST include: **Executive Summary**, **Methodology** (SQL/Python logic), **Detailed Findings** (with data tables/charts), and **Recommendations**. - 3. In your Final Answer, provide a brief summary AND the file path of the generated report. - - **Output Requirement:** - You MUST describe the detailed execution process in your final answer using **Chinese**. The description should include: - 1. **Thought Process**: How you analyzed the request and what strategy you chose. - 2. **Tool Usage**: Which tools were used, with what parameters (e.g., specific SQL queries). - 3. **Intermediate Results**: Key findings from each step (e.g., "Found customer ID ACC-001 for Xiaomi"). - 4. **Final Answer**: The direct answer to the user's question, supported by the data found. +### 任务 +您是一个AI助手,你的任务如下: +- 根据用户自然语言请求,调用工具 `recall_metadata` 查询数据库元数据,理解用户查询中涉及的数据对象、字段、过滤条件等信息。注意,调用工具 `recall_metadata`的时候,tenant参数请固定为"c360"。 +- 根据用户自然语言请求和数据库元数据生成数据可视化引擎的查询结构DSL,目标是解析用户的查询,识别所需的数据对象、字段、过滤器、排序、分组、限制。切记你构造查询结构的所有的字段信息必须从元数据中获取,不允许胡乱编造。 +- 调用工具 `query_with_dsl` 查询业务数据。注意,调用工具 `query_with_dsl` 的时候,operator参数固定为 "liujiawei.boom@bytedance.com",tenant参数请固定为"c360"。 +- 对于复杂的分析任务(例如,“分析异常”、“生成报告”、“预测趋势”、“诊断问题”),你必须: + - 使用Python进行分析。 + - **生成Markdown报告**:使用`generate_document`保存详细报告(例如,`analysis_report.md`)。 + - 报告必须包含:**执行摘要**、**方法论**(Python逻辑)、**详细发现**(含数据表格/图表)以及**建议**。 + - 在你的最终答案中,提供简要摘要以及生成报告的文件路径。 + +### 关键指南: +- **分析元数据**: + - 分析元数据,将用户描述的字段、对象或条件映射到确切的字段名。**注意** 对于使用到字段名的地方,严格按照元数据提供的字段名原样使用,不要修改,例如元数据提供的字段名= "sf_id",在使用到的地方就用"sf_id",不要修改为"sfid" + - 对于枚举字段(字段的数据类型='enum') + 1. 基于抽样值理解枚举值数据,描述结构为"value:`值`,lable:`label`" 中的label理解关键字,但始终在过滤器或条件中使用对应的value。例如:在名为'account'数据对象中,如果字段'sub_industry_c'是枚举类型,其中一个label是'游戏',value是'Game',那么如果用户说“游戏客户”,则解释为查询对象'account',过滤器为:"sub_industry_c = 'Game'"。对所有枚举应用此逻辑。 + 2. 如果使用枚举字段作为三元组判断条件,不能使用contains函数,而应该使用“=”,例如要实现“模型简称='DeepSeek'”,三元组应为"ModelShortName = 'DeepSeek'" + - 对于文本字段(字段的数据类型='text'),有以下约定 + 1. 如果同时该字段的特殊类型是“可模糊匹配”时,在过滤器条件中不能使用'='操作符,而应使用contains函数,例如name.contains('名称'),反之则不能使用contains函数 +- **解析用户查询**: + - 从用户需求中识别核心数据对象(obj)(例如,如果用户提到“客户”或“accounts”,则映射到元数据中匹配的对象)。 + - 识别字段:用于显示、过滤、排序(orderBy)、分组(groupBy)。 + - 过滤器:构建“filter”中的逻辑表达式,有以下约定: + 1. 值由三元组+逻辑连接符\大括号号嵌套连接组成,如 举例:"field1 = 'value' && (field2 > 10 or field3 = 11)"中,"field1 = 'value'"、"field2 > 10"、"field3 = 11"为三元组,"and"和"or"为逻辑连接符,"()"为嵌套逻辑 + 2. 三元组中,左值或右值可以为字段、函数、常量(如字符串、整数等),中值为比较符,如(=、>、<等) + 3. 对于日期的处理:如果用户提到“本月”,则“本月”是指当前月的第一天,将过滤器设置为日期字段 >= 当前月的第一天(格式为'YYYY-MM-01',基于当前日期计算)。 + - OrderBy:排序字段,例如"field DESC"(如果降序)。特殊规则:对于客户的查询(即query.obj = 'account'),如果用户未指定query.orderBy,则默认按照客户等级倒序排列(从元数据中映射“客户等级”字段的apiName,并设置为“ DESC”) + - GroupBy:聚合字段,例如"field"(如果求和或计数)。 + - Limit:仅整数,例如10;如果未指定,默认为100 + + - 对于客户对象的查询,有以下约定: + 1. "L6、L7"等"L级"指的是客户标签 + 2. 如果是需要按照客户名称过滤数据,默认需要使用名称和简称一起模糊搜索 + 3. 除非明确要求输出客户ID,否则不要返回 + 4. "ACC-" 这样的一串编号是指"客户编号"字段 + 5. "腾讯"指的是客户名称 + 6. **拜访/跟进时间查询**: + - 用户表述:"最近拜访时间"、"最近跟进时间"、"最新拜访日期"、"最后一次拜访"、"最后一次跟进"、"最近一次拜访是什么时候" + - 客户表的 statistical_data.AggLatestNoteSubmitTime(最近拜访日期)字段,不可排序 + - **同义词**:"拜访" = "跟进","时间" = "日期" = "是什么时候" + - ❌ 不要从拜访/跟进记录表查询或使用orderBy + - **重要**:即使用户问"最后一次跟进"或"最近一次拜访",这是描述字段含义,不代表只返回1条记录。除非用户明确要求"只看1个客户",否则limit保持默认100。 - **Planning Strategy:** - 1. **Analyze the Request**: Determine if the request requires simple database retrieval (Text-to-SQL) or complex analysis/calculation (Text-to-Python). - 2. **Formulate a Plan**: - * **Simple Path (Text-to-SQL)**: If the request is a direct data lookup (e.g., "What were the sales last month?"), create a plan to write and execute a SQL query using `run_sql`. - * **Complex Path (Text-to-Python/Multi-turn)**: If the request involves advanced analytics, predictions, complex logic, or non-SQL operations (e.g., "Predict next month's sales trend", "Find anomalies in sales"), create a multi-step plan: - a. Retrieve necessary data using `run_sql`. - b. Write a Python script using `generate_document` to process the data. - c. Execute the script using `run_python_file`. - d. Analyze the output. - 3. **Execute & Observe**: Follow your plan, executing tools and observing outputs. - 4. **Iterative Refinement (Multi-turn)**: - * **Error Recovery**: If a tool execution fails (e.g., SQL syntax error, Python runtime error), analyze the error message, revise your plan, and retry using `REPLANNING`. - * **Clarification**: If the request is ambiguous, ask the user for clarification. - - Here is the schema details of the B2B CRM database: - ```sql - CREATE TABLE IF NOT EXISTS customer ( - customer_id TEXT PRIMARY KEY, - name TEXT NOT NULL, -- Full Name - short_name TEXT, -- Short Name - is_main_customer BOOLEAN, -- Is Main Customer (1=Yes, 0=No) - customer_level TEXT, -- Customer Level (Strategic, KA, NA) - owner TEXT, -- Owner Name - sales_team TEXT, -- Sales Team - industry TEXT, - status TEXT - ); - CREATE TABLE IF NOT EXISTS revenue ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer_id TEXT, - year_month TEXT, -- Revenue Month (YYYY-MM) - product_category TEXT, -- Product Category - product_name TEXT, -- Product Name - amount REAL, -- Revenue Amount - FOREIGN KEY(customer_id) REFERENCES customer(customer_id) - ); - CREATE TABLE IF NOT EXISTS resource_usage ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer_id TEXT, - usage_date TEXT, -- Usage Date (YYYY-MM-DD) - resource_type TEXT, -- Resource Type - model_or_card TEXT, -- Model/Card Type - quantity REAL, -- Usage Quantity - FOREIGN KEY(customer_id) REFERENCES customer(customer_id) - ); - CREATE TABLE IF NOT EXISTS account_credit ( - customer_id TEXT PRIMARY KEY, - total_credit_limit REAL, -- Total Credit Limit - available_balance REAL, -- Available Balance - arrears_amount REAL, -- Arrears Amount - FOREIGN KEY(customer_id) REFERENCES customer(customer_id) - ); - ``` - - Here are some examples of how to query this database: - - Q: "小米客户近3个月的收入" (Ambiguous Name & Time Range) - Thought: User asks for "Xiaomi". I need to find the main customer "Xiaomi" to avoid "Xiaomi Shoes". "Last 3 months" refers to revenue data. - Plan: - 1. Find the `customer_id` for "Xiaomi" where `is_main_customer=1`. - 2. Query `revenue` table for this `customer_id` for the last 3 months. - A: SELECT sum(amount) FROM revenue WHERE customer_id = (SELECT customer_id FROM customer WHERE (name LIKE '%小米%' OR short_name LIKE '%小米%') AND is_main_customer=1) AND year_month >= strftime('%Y-%m', date('now', '-3 months')) - - Q: "小米最近的用量趋势" (Complex Trend Visualization) - Thought: User wants "trend". This requires daily data from `resource_usage` and a chart. - Plan: - 1. Get `customer_id` for "Xiaomi" (Main Customer). - 2. Query `usage_date` and `quantity` from `resource_usage` for the last 30 days. - 3. Save result to CSV. - 4. Call `visualize_data` to plot the trend. - A: (Plan to use `run_sql` then `visualize_data`) - - Q: "查一下分期乐的信控情况,有没有欠费?" (Derived Metric & Join) - Thought: "Debt" or "Arrears" means checking `arrears_amount` in `account_credit` table. - Plan: - 1. Find `customer_id` for "Fenqile" (分期乐). - 2. Join `customer` and `account_credit` to get credit limit, balance, and arrears. - 3. If `arrears_amount` > 0, report it as debt. - A: SELECT c.name, a.total_credit_limit, a.available_balance, a.arrears_amount FROM customer c JOIN account_credit a ON c.customer_id = a.customer_id WHERE c.name LIKE '%分期乐%' OR c.short_name LIKE '%分期乐%' - - 1. Use `run_sql` to execute queries. + - 对于用量数据的查询,有以下约定: + 1. 除了根据“大模型”、“CPU”、“GPU”这几个词来确定查询的数据对象外,还可以根据大模型用量对象中的“Model简称”字段确认本次查询是查大模型数据用量,也可以根据CPU&GPU用量对象中的“GPU卡型号”字段确认使用该对象 + 2. 如果是还要查询客户数据,则默认以客户ID作为groupBy + +### DSL构建规则 +1. filter过滤器禁止使用子查询语句。 +2.选取的元数据字段必须来自于同一数据对象,禁止跨多数据对象选取字段。 + +- 其他约定: +1. 对于100万、1亿这类的金额,在进行过滤时,需要转换成正确的数字,如100万应转换为1000000 +2. 火山账号一般为 210 开头的 10 位数字,如2100001029 +3. AppC360DmVolcengineDailyIncomeDf **不支持时间筛选,禁止添加时间条件** + +### DSL示例 +{ + "type": "object", + "properties": { + "Operator": { + "type": "string", + "description": "查询人邮箱" + }, + "Select": { + "type": "string", + "description": "要查询的字段名,多个字段用逗号分隔,类型为字符串" + }, + "Where": { + "type": "string", + "description": "过滤条件的逻辑表达式字符串,如 \"a = b or c = d\",用于筛选结果" + }, + "Limit": { + "type": "string", + "description": "返回结果的数量限制,默认10,范围1-10000" + }, + "OrderBy": { + "type": "string", + "description": "排序字段及方式,格式如“字段名 asc”表示正序,默认无排序" + }, + "Table": { + "type": "string", + "description": "查询的目标数据对象名,字符串类型,不能为空" + } + }, + "required": [ + "Operator", + "Select", + "Table" + ] +} + +### 输出要求: +你必须在最终答案中用**中文**描述详细的执行过程。描述应包括: +1. **思考过程**:你如何分析请求以及选择了何种策略。 +2. **工具使用**:使用了哪些工具,以及使用了什么参数。 +3. **中间结果**:每个步骤的关键发现。 +4. **最终答案**:对用户问题的直接回答,并辅以找到的数据支持。 """, - tools=[ - run_sql, # RunSqlTool: Execute SQL queries - visualize_data, # VisualizeDataTool: Create visualizations - save_correctanswer_memory, # SaveQuestionToolArgsTool: Save tool usage examples - search_similar_tools, # SearchSavedCorrectToolUsesTool: Search tool usage examples - generate_document, # WriteFileTool: Create new files - summarize_data, # SummarizeDataTool: Summarize CSV data - run_python_file, # RunPythonFileTool: Execute Python scripts - pip_install, # PipInstallTool: Install Python packages - read_file, # ReadFileTool: Read file content - edit_file, # EditFileTool: Edit file content - list_files, # ListFilesTool: List directory content - search_files, # SearchFilesTool: Search for files - save_text_memory # SaveTextMemoryTool: Save text to memory - ], - planner=PlanReActPlanner(), - model_extra_config={"extra_body": {"thinking": {"type": "disabled"}}} + tools=[ + run_sql, # RunSqlTool: Execute SQL queries + visualize_data, # VisualizeDataTool: Create visualizations + save_correctanswer_memory, # SaveQuestionToolArgsTool: Save tool usage examples + search_similar_tools, # SearchSavedCorrectToolUsesTool: Search tool usage examples + generate_document, # WriteFileTool: Create new files + summarize_data, # SummarizeDataTool: Summarize CSV data + run_python_file, # RunPythonFileTool: Execute Python scripts + pip_install, # PipInstallTool: Install Python packages + read_file, # ReadFileTool: Read file content + edit_file, # EditFileTool: Edit file content + list_files, # ListFilesTool: List directory content + search_files, # SearchFilesTool: Search for files + save_text_memory, # SaveTextMemoryTool: Save text to memory + query_with_dsl, + recall_metadata, + ], + planner=PlanReActPlanner(), + model_extra_config={"extra_body": {"thinking": {"type": "disabled"}}}, ) diff --git a/examples/veadk-vanna-proj/src/data_agent/tools.py b/examples/veadk-vanna-proj/src/data_agent/tools.py index 377ea510..c65a8f42 100644 --- a/examples/veadk-vanna-proj/src/data_agent/tools.py +++ b/examples/veadk-vanna-proj/src/data_agent/tools.py @@ -1,45 +1,44 @@ import os import httpx -import asyncio import pandas as pd import io from typing import Optional, Dict, Any +import requests from vanna.integrations.sqlite import SqliteRunner from vanna.tools.file_system import ( - LocalFileSystem, + LocalFileSystem, WriteFileTool, ReadFileTool, EditFileTool, ListFilesTool, - SearchFilesTool -) -from vanna.tools import ( - RunSqlTool, - VisualizeDataTool + SearchFilesTool, ) +from vanna.tools import RunSqlTool, VisualizeDataTool from vanna.tools.python import RunPythonFileTool, PipInstallTool from vanna.tools.agent_memory import ( - SaveQuestionToolArgsTool, - SearchSavedCorrectToolUsesTool, - SaveTextMemoryTool + SaveQuestionToolArgsTool, + SearchSavedCorrectToolUsesTool, + SaveTextMemoryTool, ) from vanna.integrations.local.agent_memory import DemoAgentMemory from vanna.core.tool import ToolContext -from vanna.core.registry import ToolRegistry from vanna.core.user import User + # Setup SQLite def setup_sqlite(): # Use the generated B2B sample data # Note: In VeFaaS, only /tmp is writable, so we might need to copy it there if we want to modify it. # But for read-only access or local dev, we can point to the sample_data directory. - + # Try to find the sample data relative to this file current_dir = os.path.dirname(os.path.abspath(__file__)) # Go up one level to src, then to sample_data - sample_data_path = os.path.join(os.path.dirname(current_dir), 'sample_data', 'b2b_crm.sqlite') - + sample_data_path = os.path.join( + os.path.dirname(current_dir), "sample_data", "b2b_crm.sqlite" + ) + if os.path.exists(sample_data_path): return sample_data_path @@ -57,6 +56,7 @@ def setup_sqlite(): print(f"Error downloading database: {e}") return db_path + # Initialize Resources db_path = setup_sqlite() # Use /tmp for file storage as it's the only writable directory in VeFaaS @@ -86,20 +86,23 @@ def setup_sqlite(): # Create a mock context for tool execution # In a real application, this should be created per-request with the actual user -mock_user = User(id="veadk-user", email="user@example.com", group_memberships=["admin", "user"]) +mock_user = User( + id="veadk-user", email="user@example.com", group_memberships=["admin", "user"] +) mock_context = ToolContext( - user=mock_user, - conversation_id="default", - request_id="default", - agent_memory=agent_memory + user=mock_user, + conversation_id="default", + request_id="default", + agent_memory=agent_memory, ) # Wrapper Functions for Veadk Agent + async def run_sql(sql: str) -> str: """ Execute a SQL query against the Chinook database. - + Args: sql: The SQL query to execute. """ @@ -107,41 +110,46 @@ async def run_sql(sql: str) -> str: result = await sql_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def visualize_data(filename: str, title: str = None) -> str: """ Visualize data from a CSV file. - + Args: filename: The name of the CSV file to visualize. title: Optional title for the chart. """ # Check if the file is likely a CSV file - if not filename.lower().endswith('.csv'): - return f"Error: visualize_data only supports CSV files. You provided: {filename}" - + if not filename.lower().endswith(".csv"): + return ( + f"Error: visualize_data only supports CSV files. You provided: {filename}" + ) + args_model = viz_tool.get_args_schema()(filename=filename, title=title) result = await viz_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def run_python_file(filename: str) -> str: """ Execute a Python file. - + Args: filename: The name of the Python file to execute. """ # Check if the file is likely a Python file - if not filename.lower().endswith('.py'): + if not filename.lower().endswith(".py"): return f"Error: run_python_file only supports Python files. You provided: {filename}" args_model = run_python_tool.get_args_schema()(filename=filename) result = await run_python_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def pip_install(packages: list[str]) -> str: """ Install Python packages using pip. - + Args: packages: List of package names to install. """ @@ -149,23 +157,27 @@ async def pip_install(packages: list[str]) -> str: result = await pip_install_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def read_file(filename: str, start_line: int = 1, end_line: int = -1) -> str: """ Read the content of a file. - + Args: filename: The name of the file to read. start_line: The line number to start reading from (1-based). end_line: The line number to stop reading at (inclusive). -1 for end of file. """ - args_model = read_file_tool.get_args_schema()(filename=filename, start_line=start_line, end_line=end_line) + args_model = read_file_tool.get_args_schema()( + filename=filename, start_line=start_line, end_line=end_line + ) result = await read_file_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def edit_file(filename: str, edits: list[dict[str, Any]]) -> str: """ Edit a file by replacing lines. - + Args: filename: The name of the file to edit. edits: A list of edits to apply. Each edit is a dictionary with: @@ -178,10 +190,11 @@ async def edit_file(filename: str, edits: list[dict[str, Any]]) -> str: result = await edit_file_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def list_files(path: str = ".") -> str: """ List files in a directory. - + Args: path: The directory path to list. Defaults to current directory. """ @@ -189,10 +202,11 @@ async def list_files(path: str = ".") -> str: result = await list_files_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def search_files(query: str, path: str = ".") -> str: """ Search for files matching a query. - + Args: query: The search query (regex or glob pattern). path: The directory path to search in. Defaults to current directory. @@ -201,10 +215,13 @@ async def search_files(query: str, path: str = ".") -> str: result = await search_files_tool.execute(mock_context, args_model) return str(result.result_for_llm) -async def save_correctanswer_memory(question: str, tool_name: str, args: Dict[str, Any]) -> str: + +async def save_correctanswer_memory( + question: str, tool_name: str, args: Dict[str, Any] +) -> str: """ Save a successful question-tool-argument combination for future reference. - + Args: question: The original question that was asked. tool_name: The name of the tool that was used successfully. @@ -213,10 +230,11 @@ async def save_correctanswer_memory(question: str, tool_name: str, args: Dict[st # Temporarily disabled due to infinite loop issues return "Memory saved successfully (Simulated)" + async def search_similar_tools(question: str, limit: int = 10) -> str: """ Search for similar tool usage patterns based on a question. - + Args: question: The question to find similar tool usage patterns for. limit: Maximum number of results to return. @@ -226,10 +244,11 @@ async def search_similar_tools(question: str, limit: int = 10) -> str: # Return the result (whether success or error message) return str(result.result_for_llm) + async def save_text_memory(text: str, tags: list[str] = None) -> str: """ Save arbitrary text to memory for future retrieval. - + Args: text: The text content to save. tags: Optional list of tags to categorize the memory. @@ -240,38 +259,239 @@ async def save_text_memory(text: str, tags: list[str] = None) -> str: result = await save_text_mem_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def generate_document(filename: str, content: str) -> str: """ Generate a document (save content to a file). - + Args: filename: The name of the file to save (e.g., 'report.md', 'summary.txt'). content: The text content to write to the file. """ - args_model = write_file_tool.get_args_schema()(filename=filename, content=content, overwrite=True) + args_model = write_file_tool.get_args_schema()( + filename=filename, content=content, overwrite=True + ) result = await write_file_tool.execute(mock_context, args_model) return str(result.result_for_llm) + async def summarize_data(filename: str) -> str: """ Generate a statistical summary of data from a CSV file. - + Args: filename: The name of the CSV file to summarize. """ try: # Read the file content content = await file_system.read_file(filename, mock_context) - + # Parse into DataFrame df = pd.read_csv(io.StringIO(content)) - + # Generate summary stats description = df.describe().to_markdown() head = df.head().to_markdown() info = f"Rows: {len(df)}, Columns: {len(df.columns)}\nColumn Names: {', '.join(df.columns)}" - + summary = f"**Data Summary for {filename}**\n\n**Info:**\n{info}\n\n**First 5 Rows:**\n{head}\n\n**Statistical Description:**\n{description}" return summary except Exception as e: return f"Failed to summarize data: {str(e)}" + + +# def query_with_dsl(dsl_json: Dict[str, Any], timeout: int = 30) -> Dict[str, Any]: +# """ +# 使用DSL JSON查询数据的函数 + +# Args: +# dsl_json: 完整的DSL查询JSON对象 +# timeout: 请求超时时间(秒) + +# Returns: +# 格式化后的查询结果字典 + +# Example: +# dsl = { +# "Operator": "liujiawei.boom@bytedance.com", +# "Tenant": "c360", +# "Table": "large_model_usage", +# "Select": "account_number, request_date, token_amount", +# "GroupBy": "account_number, request_date", +# "Where": "request_date >= '2025-11-24' and account_number != 'ACC-0000872346'", +# "OrderBy": "", +# "Limit": 100 +# } +# result = query_with_dsl(dsl) +# """ +# # API端点 +# host = "bytedance" +# url = f"http://eps-agent.{host}.net/search_metadata/query?Action=Query" + +# # 请求头 +# headers = { +# "Content-Type": "application/json" +# } + +# try: +# # 发送POST请求 +# response = requests.post(url, headers=headers, json=dsl_json, timeout=timeout) + +# # 检查响应状态 +# response.raise_for_status() + +# # 解析JSON响应 +# result = response.json() + +# # 格式化输出 +# formatted_result = { +# "ResponseMetadata": { +# "RequestId": result.get("ResponseMetadata", {}).get("RequestId", "") +# }, +# "Result": [] +# } + +# # 提取结果数据 +# if "Result" in result: +# for item in result["Result"]: +# formatted_item = { +# "account_id": item.get("account_id", ""), +# "account_number": item.get("account_number", ""), +# "request_date": item.get("request_date", ""), +# "token_amount": item.get("token_amount", 0) +# } +# formatted_result["Result"].append(formatted_item) + +# return formatted_result + +# except requests.exceptions.RequestException as e: +# raise Exception(f"请求错误: {e}") +# except json.JSONDecodeError as e: +# raise Exception(f"JSON解析错误: {e}") +# except Exception as e: +# raise Exception(f"未知错误: {e}") + + +def query_with_dsl( + operator: str, + tenant: str, + table: str, + select: str, + group_by: Optional[str] = None, + where: Optional[str] = None, + order_by: Optional[str] = None, + limit: Optional[int] = 100, + timeout: int = 30, +) -> Dict[str, Any]: + """ + 查询数据的函数 + + Args: + operator: 操作者标识,通常为企业邮箱,用于审计和权限校验 + tenant: 租户标识,需与元数据查询时保持一致 + table: 需要查询的数据表名 + select: 需要查询的字段列表,多个字段间用英文逗号分隔 + group_by: 分组字段列表,多个字段间用英文逗号分隔 + where: 筛选条件,采用 SQL-like 语法 + order_by: 排序条件,格式为 "字段名 ASC/DESC" + limit: 返回记录的最大数量,默认为 100 + timeout: 请求超时时间(秒) + + Returns: + 查询结果字典 + + Example: + result = query_data( + operator="liujiawei.boom@bytedance.com", + tenant="c360", + table="large_model_usage", + select="account_number, request_date, token_amount", + group_by="account_number, request_date", + where="request_date >= '2025-11-24' and account_number != 'ACC-0000872346'", + order_by="request_date DESC", + limit=100 + ) + """ + # API端点 + host = "bytedance" + url = f"http://eps-agent.{host}.net/search_metadata/query?Action=Query" + + # 构建请求体 + payload = { + "Operator": operator, + "Tenant": tenant, + "Table": table, + "Select": select, + } + + # 添加可选参数 + if group_by: + payload["GroupBy"] = group_by + if where: + payload["Where"] = where + if order_by: + payload["OrderBy"] = order_by + if limit is not None: + payload["Limit"] = limit + + # 请求头 + headers = {"Content-Type": "application/json"} + + try: + # 发送POST请求 + response = requests.post(url, headers=headers, json=payload, timeout=timeout) + + # 解析JSON响应 + result = response.json() + + # 检查响应状态 + response.raise_for_status() + + return result + + except Exception as e: + return f"错误: {e}, 返回内容: {result}" + + +def recall_metadata(tenant: str, query: str, timeout: int = 30) -> Dict[str, Any]: + """ + 调用元数据查询接口的函数 + + Args: + tenant: 租户名称 + query: 查询文本 + timeout: 请求超时时间(秒) + + Returns: + 查询结果字典 + + Example: + result = recall_metadata( + tenant="c360", + query="小米的收入是多少?" + ) + """ + # API端点 + host = "bytedance" + url = f"http://eps-agent.{host}.net/search_metadata/metadata?Action=RecallMetadata" + + # 请求头 + headers = {"Content-Type": "application/json"} + + # 请求体 + payload = {"Tenant": tenant, "Query": query} + + try: + # 发送POST请求 + response = requests.post(url, headers=headers, json=payload, timeout=timeout) + + result = response.json() + + # 检查响应状态 + response.raise_for_status() + + # 解析JSON响应 + return result + + except requests.exceptions.RequestException as e: + return f"错误: {e}, 返回内容: {result}" From 4b548083f63952c9ecd973028e037f119d23ff8d Mon Sep 17 00:00:00 2001 From: "wuqingfu.528" Date: Wed, 11 Feb 2026 21:29:21 +0800 Subject: [PATCH 6/6] fix: add time tool --- examples/veadk-vanna-proj/src/data_agent/agent.py | 2 ++ examples/veadk-vanna-proj/src/data_agent/tools.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/examples/veadk-vanna-proj/src/data_agent/agent.py b/examples/veadk-vanna-proj/src/data_agent/agent.py index c739b348..3a392169 100644 --- a/examples/veadk-vanna-proj/src/data_agent/agent.py +++ b/examples/veadk-vanna-proj/src/data_agent/agent.py @@ -16,6 +16,7 @@ save_text_memory, query_with_dsl, recall_metadata, + get_current_time, ) # # Define the Veadk Agent using Vanna Tools @@ -290,6 +291,7 @@ save_text_memory, # SaveTextMemoryTool: Save text to memory query_with_dsl, recall_metadata, + get_current_time, ], planner=PlanReActPlanner(), model_extra_config={"extra_body": {"thinking": {"type": "disabled"}}}, diff --git a/examples/veadk-vanna-proj/src/data_agent/tools.py b/examples/veadk-vanna-proj/src/data_agent/tools.py index c65a8f42..188f92fe 100644 --- a/examples/veadk-vanna-proj/src/data_agent/tools.py +++ b/examples/veadk-vanna-proj/src/data_agent/tools.py @@ -495,3 +495,18 @@ def recall_metadata(tenant: str, query: str, timeout: int = 30) -> Dict[str, Any except requests.exceptions.RequestException as e: return f"错误: {e}, 返回内容: {result}" + + +def get_current_time() -> str: + """ + 获取当前时间的函数 + + Returns: + 当前时间的字符串表示,格式为 "YYYY-MM-DD HH:MM:SS" + + Example: + current_time = get_current_time() + """ + from datetime import datetime + + return datetime.now().strftime("%Y-%m-%d %H:%M:%S")