Spaces:
Sleeping
Sleeping
Commit ·
6bd3e57
0
Parent(s):
Refactor code structure and remove redundant sections for improved readability and maintainability
Browse files- .github/copilot-instructions.md +12 -0
- .gitignore +94 -0
- README.md +21 -0
- pyproject.toml +35 -0
- src/sales_assistant/__init__.py +1 -0
- src/sales_assistant/agent_main/__init__.py +1 -0
- src/sales_assistant/agent_main/agent_graph.py +49 -0
- src/sales_assistant/agent_main/agent_node.py +71 -0
- src/sales_assistant/agent_main/agent_runner.py +262 -0
- src/sales_assistant/agent_main/tools_node.py +43 -0
- src/sales_assistant/agent_tools/__init__.py +1 -0
- src/sales_assistant/agent_tools/agent_tools_utils.py +69 -0
- src/sales_assistant/agent_tools/create_quote.py +348 -0
- src/sales_assistant/agent_tools/describe_table.py +36 -0
- src/sales_assistant/agent_tools/execute_advanced_query.py +37 -0
- src/sales_assistant/agent_tools/execute_sql_query.py +79 -0
- src/sales_assistant/agent_tools/get_distinct_values.py +37 -0
- src/sales_assistant/agent_tools/get_exchange_rates.py +66 -0
- src/sales_assistant/agent_tools/get_product_by_criteria.py +36 -0
- src/sales_assistant/agent_tools/get_samples_data.py +57 -0
- src/sales_assistant/agent_tools/get_table_statistics.py +36 -0
- src/sales_assistant/agent_tools/quote_template.py +242 -0
- src/sales_assistant/created_quotes/quote_QT-B4849588_Kristof_Proba_20250825_114359.md +63 -0
- src/sales_assistant/db/utils/db_connections.py +105 -0
- src/sales_assistant/db/utils/db_utils.py +75 -0
- src/sales_assistant/main.py +226 -0
- src/sales_assistant/prompts/__init__.py +0 -0
- src/sales_assistant/prompts/system_prompt.py +141 -0
- test_files/db_test.py +81 -0
- test_files/quote_test.py +171 -0
- test_files/test_product_id_quotes.py +158 -0
- test_files/tools_test.py +184 -0
- uv.lock +0 -0
.github/copilot-instructions.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This project is using uv so you can run the files using the uv commands.
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
Be sure to:
|
| 5 |
+
if you are using llm agents use the langchain or langgraph built in framework as much as possbile with states and pydantic if needed.
|
| 6 |
+
Do NOT use custom written function if possible.
|
| 7 |
+
Use this document if needed with the offical documentation:
|
| 8 |
+
https://langchain-ai.github.io/langgraph/concepts/low_level/
|
| 9 |
+
https://langchain-ai.github.io/langgraph/how-tos/graph-api/
|
| 10 |
+
|
| 11 |
+
There is a mysql database (called: streamnet_prodcut_try) with the following tables and columns.
|
| 12 |
+
You will use this in the chatbot retrieve data.
|
.gitignore
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment files
|
| 2 |
+
*.env
|
| 3 |
+
.env*
|
| 4 |
+
|
| 5 |
+
# Python
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
+
*$py.class
|
| 9 |
+
*.so
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
*.manifest
|
| 31 |
+
*.spec
|
| 32 |
+
|
| 33 |
+
# Installer logs
|
| 34 |
+
pip-log.txt
|
| 35 |
+
pip-delete-this-directory.txt
|
| 36 |
+
|
| 37 |
+
# Unit test / coverage reports
|
| 38 |
+
htmlcov/
|
| 39 |
+
.tox/
|
| 40 |
+
.nox/
|
| 41 |
+
.coverage
|
| 42 |
+
.coverage.*
|
| 43 |
+
.cache
|
| 44 |
+
nosetests.xml
|
| 45 |
+
coverage.xml
|
| 46 |
+
*.cover
|
| 47 |
+
*.py,cover
|
| 48 |
+
.hypothesis/
|
| 49 |
+
.pytest_cache/
|
| 50 |
+
cover/
|
| 51 |
+
|
| 52 |
+
# Jupyter Notebook
|
| 53 |
+
.ipynb_checkpoints
|
| 54 |
+
*checkpoint*
|
| 55 |
+
|
| 56 |
+
# IPython
|
| 57 |
+
profile_default/
|
| 58 |
+
ipython_config.py
|
| 59 |
+
|
| 60 |
+
# PyEnv
|
| 61 |
+
.python-version
|
| 62 |
+
|
| 63 |
+
# UV
|
| 64 |
+
.venv/
|
| 65 |
+
venv/
|
| 66 |
+
ENV/
|
| 67 |
+
env/
|
| 68 |
+
|
| 69 |
+
# VS Code
|
| 70 |
+
.vscode/
|
| 71 |
+
*.code-workspace
|
| 72 |
+
|
| 73 |
+
# IDE
|
| 74 |
+
.idea/
|
| 75 |
+
*.swp
|
| 76 |
+
*.swo
|
| 77 |
+
*~
|
| 78 |
+
|
| 79 |
+
# OS
|
| 80 |
+
.DS_Store
|
| 81 |
+
.DS_Store?
|
| 82 |
+
._*
|
| 83 |
+
.Spotlight-V100
|
| 84 |
+
.Trashes
|
| 85 |
+
ehthumbs.db
|
| 86 |
+
Thumbs.db
|
| 87 |
+
|
| 88 |
+
# Project specific
|
| 89 |
+
*.pem
|
| 90 |
+
*.log
|
| 91 |
+
|
| 92 |
+
# Temporary files
|
| 93 |
+
*.tmp
|
| 94 |
+
*.temp
|
README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sales Assistant with Quote
|
| 2 |
+
|
| 3 |
+
A sales bot that can answer questions about products and provide quotes based on a database.
|
| 4 |
+
|
| 5 |
+
## Installation
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
uv sync
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
Or if you want to install it in editable mode:
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
uv pip install -e .
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
## To try out the sales assistant, run:
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
uv run python src/sales_assistant/main.py
|
| 21 |
+
```
|
pyproject.toml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "sales-assistant-with-quote"
|
| 3 |
+
version = "1.0.0"
|
| 4 |
+
description = "A sales bot that can answer questions about products and provide quotes based on a database."
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"pandas>=2.3.1",
|
| 9 |
+
"pymysql>=1.1.1",
|
| 10 |
+
"python-dotenv>=1.1.1",
|
| 11 |
+
"sqlalchemy>=2.0.41",
|
| 12 |
+
"sshtunnel>=0.4.0",
|
| 13 |
+
"paramiko>=3.0.0,<4.0.0",
|
| 14 |
+
# for jupyter notebooks
|
| 15 |
+
"jupyter>=1.0.0",
|
| 16 |
+
"ipykernel>=6.25.2",
|
| 17 |
+
"langchain>=0.3.27",
|
| 18 |
+
"langgraph>=0.6.5",
|
| 19 |
+
"langchain-openai>=0.3.30",
|
| 20 |
+
"langchain-nvidia-ai-endpoints>=0.3.0",
|
| 21 |
+
"langsmith>=0.2.13",
|
| 22 |
+
"pydantic>=2.11.7",
|
| 23 |
+
"huggingface-hub>=0.34.4",
|
| 24 |
+
"langchain-huggingface>=0.3.1",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[build-system]
|
| 28 |
+
requires = ["setuptools>=61.0", "wheel"]
|
| 29 |
+
build-backend = "setuptools.build_meta"
|
| 30 |
+
|
| 31 |
+
[tool.setuptools.packages.find]
|
| 32 |
+
where = ["src"]
|
| 33 |
+
|
| 34 |
+
[tool.setuptools.package-dir]
|
| 35 |
+
"" = "src"
|
src/sales_assistant/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Sales Assistant Package
|
src/sales_assistant/agent_main/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Sales Assistant Package
|
src/sales_assistant/agent_main/agent_graph.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import uuid
|
| 4 |
+
from typing import List, Dict, Any, Literal
|
| 5 |
+
from langchain_core.tools import tool
|
| 6 |
+
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
| 7 |
+
from langgraph.graph import StateGraph, MessagesState, START, END
|
| 8 |
+
from langgraph.prebuilt import ToolNode, tools_condition
|
| 9 |
+
from langchain.chat_models import init_chat_model
|
| 10 |
+
from langsmith import Client
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from .agent_node import agent_node
|
| 13 |
+
from .tools_node import tool_node
|
| 14 |
+
|
| 15 |
+
def create_agent_graph(checkpointer=None):
|
| 16 |
+
"""
|
| 17 |
+
Create the ReAct database exploration agent using LangGraph's built-in components.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
checkpointer: Optional checkpointer for state persistence
|
| 21 |
+
"""
|
| 22 |
+
# Initialize StateGraph with built-in MessagesState
|
| 23 |
+
workflow = StateGraph(MessagesState)
|
| 24 |
+
|
| 25 |
+
# Add nodes using built-in components
|
| 26 |
+
workflow.add_node("agent", agent_node)
|
| 27 |
+
workflow.add_node("tools", tool_node)
|
| 28 |
+
|
| 29 |
+
# Add edges using built-in routing
|
| 30 |
+
workflow.add_edge(START, "agent")
|
| 31 |
+
|
| 32 |
+
# Use built-in tools_condition for conditional routing
|
| 33 |
+
workflow.add_conditional_edges(
|
| 34 |
+
"agent",
|
| 35 |
+
tools_condition, # Built-in condition that checks for tool calls
|
| 36 |
+
{
|
| 37 |
+
"tools": "tools",
|
| 38 |
+
END: END,
|
| 39 |
+
}
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Tools always return to agent for continued reasoning
|
| 43 |
+
workflow.add_edge("tools", "agent")
|
| 44 |
+
|
| 45 |
+
# Compile the graph with optional checkpointer
|
| 46 |
+
if checkpointer:
|
| 47 |
+
return workflow.compile(checkpointer=checkpointer)
|
| 48 |
+
else:
|
| 49 |
+
return workflow.compile()
|
src/sales_assistant/agent_main/agent_node.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Basis agent node that creates the initial agent with the system prompt.
|
| 3 |
+
Need to use the prompt from the prompts/system_prompt.py file
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
from langchain_core.messages import SystemMessage
|
| 8 |
+
from langgraph.graph import MessagesState
|
| 9 |
+
from langchain_openai import ChatOpenAI
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from ..prompts.system_prompt import SYSTEM_PROMPT
|
| 12 |
+
|
| 13 |
+
# Load environment variables
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
def agent_node(state: MessagesState) -> Dict[str, Any]:
|
| 17 |
+
"""
|
| 18 |
+
Agent node that processes messages and generates responses using the LLM.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
state: MessagesState containing the conversation history
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
Dict containing the updated messages
|
| 25 |
+
"""
|
| 26 |
+
# Get model configuration from environment
|
| 27 |
+
model_provider = os.getenv("MODEL_PROVIDER", "openai").lower()
|
| 28 |
+
model_name = os.getenv("MODEL_NAME", "gpt-5-mini")
|
| 29 |
+
|
| 30 |
+
# Initialize the chat model based on provider
|
| 31 |
+
if model_provider == "openai":
|
| 32 |
+
# Check if model supports temperature
|
| 33 |
+
if "gpt-5" in model_name:
|
| 34 |
+
# GPT-5 models don't have temperature parameter
|
| 35 |
+
model = ChatOpenAI(
|
| 36 |
+
model=model_name,
|
| 37 |
+
api_key=os.getenv("OPENAI_API_KEY"),
|
| 38 |
+
)
|
| 39 |
+
else:
|
| 40 |
+
# Other models with temperature support
|
| 41 |
+
temperature = float(os.getenv("MODEL_TEMPERATURE", "0.1"))
|
| 42 |
+
model = ChatOpenAI(
|
| 43 |
+
model=model_name,
|
| 44 |
+
api_key=os.getenv("OPENAI_API_KEY"),
|
| 45 |
+
temperature=temperature,
|
| 46 |
+
)
|
| 47 |
+
else:
|
| 48 |
+
# Fallback to OpenAI if other providers not implemented
|
| 49 |
+
model = ChatOpenAI(
|
| 50 |
+
model=model_name,
|
| 51 |
+
api_key=os.getenv("OPENAI_API_KEY"),
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Bind tools to the model
|
| 55 |
+
from .tools_node import get_all_tools
|
| 56 |
+
tools = get_all_tools()
|
| 57 |
+
model_with_tools = model.bind_tools(tools)
|
| 58 |
+
|
| 59 |
+
# Get the current messages
|
| 60 |
+
messages = state["messages"]
|
| 61 |
+
|
| 62 |
+
# Add system prompt if it's the first message or not present
|
| 63 |
+
if not messages or not isinstance(messages[0], SystemMessage):
|
| 64 |
+
system_message = SystemMessage(content=SYSTEM_PROMPT)
|
| 65 |
+
messages = [system_message] + messages
|
| 66 |
+
|
| 67 |
+
# Generate response
|
| 68 |
+
response = model_with_tools.invoke(messages)
|
| 69 |
+
|
| 70 |
+
# Return the updated state
|
| 71 |
+
return {"messages": [response]}
|
src/sales_assistant/agent_main/agent_runner.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
from typing import Dict, Any, Optional, List
|
| 4 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
| 5 |
+
from langgraph.graph import MessagesState
|
| 6 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 7 |
+
from langsmith import Client
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from .agent_graph import create_agent_graph
|
| 11 |
+
|
| 12 |
+
# Load environment variables
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ConversationConfig(BaseModel):
|
| 17 |
+
"""Configuration for the conversation."""
|
| 18 |
+
session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 19 |
+
user_id: str = Field(default="default_user")
|
| 20 |
+
langsmith_project: str = Field(default_factory=lambda: os.getenv("LANGSMITH_PROJECT", "sales-assistant"))
|
| 21 |
+
enable_langsmith: bool = Field(default=True)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def setup_langsmith_tracing(config: ConversationConfig) -> Optional[Client]:
|
| 25 |
+
"""
|
| 26 |
+
Setup LangSmith tracing for the conversation.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
config: Configuration for the conversation
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
LangSmith client if enabled, None otherwise
|
| 33 |
+
"""
|
| 34 |
+
if not config.enable_langsmith:
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# Set environment variables for LangSmith using the correct env var names
|
| 39 |
+
if os.getenv("LANGSMITH_API_KEY"):
|
| 40 |
+
os.environ["LANGCHAIN_TRACING_V2"] = os.getenv("LANGSMITH_TRACING_V2", "true")
|
| 41 |
+
os.environ["LANGCHAIN_PROJECT"] = config.langsmith_project
|
| 42 |
+
os.environ["LANGCHAIN_ENDPOINT"] = os.getenv("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
|
| 43 |
+
os.environ["LANGCHAIN_API_KEY"] = os.getenv("LANGSMITH_API_KEY")
|
| 44 |
+
|
| 45 |
+
client = Client()
|
| 46 |
+
return client
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Warning: Could not setup LangSmith tracing: {e}")
|
| 49 |
+
|
| 50 |
+
return None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def create_agent_runner(config: ConversationConfig = None) -> tuple:
|
| 54 |
+
"""
|
| 55 |
+
Create an agent runner with the compiled graph and LangGraph's built-in memory.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
config: Optional configuration for the conversation
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Tuple of (compiled_graph, checkpointer, langsmith_client, thread_id)
|
| 62 |
+
"""
|
| 63 |
+
if config is None:
|
| 64 |
+
config = ConversationConfig()
|
| 65 |
+
|
| 66 |
+
# Setup LangSmith tracing
|
| 67 |
+
langsmith_client = setup_langsmith_tracing(config)
|
| 68 |
+
|
| 69 |
+
# Create LangGraph's built-in memory checkpointer
|
| 70 |
+
checkpointer = MemorySaver()
|
| 71 |
+
|
| 72 |
+
# Create the agent graph with checkpointing
|
| 73 |
+
compiled_graph = create_agent_graph(checkpointer)
|
| 74 |
+
|
| 75 |
+
# Use session_id as thread_id for LangGraph's memory
|
| 76 |
+
thread_id = config.session_id
|
| 77 |
+
|
| 78 |
+
return compiled_graph, checkpointer, langsmith_client, thread_id
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def run_conversation_turn(
|
| 82 |
+
compiled_graph,
|
| 83 |
+
thread_id: str,
|
| 84 |
+
user_input: str,
|
| 85 |
+
langsmith_client: Optional[Client] = None
|
| 86 |
+
) -> str:
|
| 87 |
+
"""
|
| 88 |
+
Run a single conversation turn with the agent using LangGraph's built-in state management.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
compiled_graph: The compiled LangGraph agent with checkpointer
|
| 92 |
+
thread_id: Thread ID for conversation persistence
|
| 93 |
+
user_input: User's input message
|
| 94 |
+
langsmith_client: Optional LangSmith client for tracing
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Agent's response as a string
|
| 98 |
+
"""
|
| 99 |
+
try:
|
| 100 |
+
# Create user message
|
| 101 |
+
user_message = HumanMessage(content=user_input)
|
| 102 |
+
|
| 103 |
+
# Create the config for this conversation thread
|
| 104 |
+
config = {"configurable": {"thread_id": thread_id}}
|
| 105 |
+
|
| 106 |
+
# Invoke the graph with the user message
|
| 107 |
+
# LangGraph will automatically handle state persistence
|
| 108 |
+
result = compiled_graph.invoke(
|
| 109 |
+
{"messages": [user_message]},
|
| 110 |
+
config=config
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# Get the assistant's response
|
| 114 |
+
assistant_message = result["messages"][-1]
|
| 115 |
+
assistant_response = assistant_message.content
|
| 116 |
+
|
| 117 |
+
return assistant_response
|
| 118 |
+
|
| 119 |
+
except Exception as e:
|
| 120 |
+
error_message = f"Error processing message: {str(e)}"
|
| 121 |
+
print(error_message)
|
| 122 |
+
return error_message
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def run_interactive_loop(config: ConversationConfig = None) -> None:
|
| 126 |
+
"""
|
| 127 |
+
Run an interactive conversation loop with the agent using LangGraph's built-in state management.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
config: Optional configuration for the conversation
|
| 131 |
+
"""
|
| 132 |
+
if config is None:
|
| 133 |
+
config = ConversationConfig()
|
| 134 |
+
|
| 135 |
+
# Create agent runner with LangGraph's built-in memory
|
| 136 |
+
compiled_graph, checkpointer, langsmith_client, thread_id = create_agent_runner(config)
|
| 137 |
+
|
| 138 |
+
print("🤖 Sales Assistant initialized!")
|
| 139 |
+
print(f"Using the following llm model: {os.getenv('MODEL_NAME', 'not set in .env')}")
|
| 140 |
+
print("💬 Type your questions about products or 'quit' to exit")
|
| 141 |
+
print(f"📊 Session ID: {config.session_id}")
|
| 142 |
+
print(f"🧵 Thread ID: {thread_id}")
|
| 143 |
+
if langsmith_client:
|
| 144 |
+
print(f"📈 LangSmith tracking enabled - Project: {config.langsmith_project}")
|
| 145 |
+
print("-" * 50)
|
| 146 |
+
|
| 147 |
+
while True:
|
| 148 |
+
try:
|
| 149 |
+
user_input = input("\n👤 You: ").strip()
|
| 150 |
+
|
| 151 |
+
if user_input.lower() in ['quit', 'exit', 'bye']:
|
| 152 |
+
print("👋 Goodbye!")
|
| 153 |
+
break
|
| 154 |
+
|
| 155 |
+
if not user_input:
|
| 156 |
+
continue
|
| 157 |
+
|
| 158 |
+
print("🔄 Processing...")
|
| 159 |
+
response = run_conversation_turn(
|
| 160 |
+
compiled_graph,
|
| 161 |
+
thread_id,
|
| 162 |
+
user_input,
|
| 163 |
+
langsmith_client
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
print(f"\n🤖 Assistant: {response}")
|
| 167 |
+
|
| 168 |
+
except KeyboardInterrupt:
|
| 169 |
+
print("\n👋 Goodbye!")
|
| 170 |
+
break
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"\n❌ Error: {str(e)}")
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def get_conversation_history(compiled_graph, thread_id: str) -> List[Dict[str, Any]]:
|
| 176 |
+
"""
|
| 177 |
+
Get the conversation history from LangGraph's checkpointer.
|
| 178 |
+
|
| 179 |
+
Args:
|
| 180 |
+
compiled_graph: The compiled graph with checkpointer
|
| 181 |
+
thread_id: Thread ID for conversation
|
| 182 |
+
|
| 183 |
+
Returns:
|
| 184 |
+
List of messages in the conversation
|
| 185 |
+
"""
|
| 186 |
+
try:
|
| 187 |
+
config = {"configurable": {"thread_id": thread_id}}
|
| 188 |
+
# Get the current state from LangGraph's checkpointer
|
| 189 |
+
state = compiled_graph.get_state(config)
|
| 190 |
+
messages = state.values.get("messages", [])
|
| 191 |
+
|
| 192 |
+
# Convert messages to dict format
|
| 193 |
+
history = []
|
| 194 |
+
for msg in messages:
|
| 195 |
+
if hasattr(msg, "type"):
|
| 196 |
+
history.append({
|
| 197 |
+
"type": msg.type,
|
| 198 |
+
"content": msg.content,
|
| 199 |
+
"timestamp": getattr(msg, "id", str(uuid.uuid4()))
|
| 200 |
+
})
|
| 201 |
+
return history
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"Error getting conversation history: {e}")
|
| 204 |
+
return []
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def clear_conversation_history(compiled_graph, thread_id: str) -> None:
|
| 208 |
+
"""
|
| 209 |
+
Clear the conversation history in LangGraph's checkpointer.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
compiled_graph: The compiled graph with checkpointer
|
| 213 |
+
thread_id: Thread ID for conversation
|
| 214 |
+
"""
|
| 215 |
+
try:
|
| 216 |
+
config = {"configurable": {"thread_id": thread_id}}
|
| 217 |
+
# Note: LangGraph's MemorySaver doesn't have a direct clear method
|
| 218 |
+
# You would need to implement this based on your specific checkpointer
|
| 219 |
+
print(f"Note: To clear history, restart with a new thread_id")
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"Error clearing conversation history: {e}")
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# Example usage function
|
| 225 |
+
def demo_agent_runner():
|
| 226 |
+
"""
|
| 227 |
+
Demo function to show how to use the agent runner with LangGraph's built-in state management.
|
| 228 |
+
"""
|
| 229 |
+
# Create configuration
|
| 230 |
+
config = ConversationConfig(
|
| 231 |
+
session_id="demo_session",
|
| 232 |
+
user_id="demo_user",
|
| 233 |
+
langsmith_project="sales-assistant-demo"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Create agent runner with LangGraph's built-in memory
|
| 237 |
+
compiled_graph, checkpointer, langsmith_client, thread_id = create_agent_runner(config)
|
| 238 |
+
|
| 239 |
+
# Run a few example interactions
|
| 240 |
+
test_queries = [
|
| 241 |
+
"What products do you have from Samsung?",
|
| 242 |
+
"Show me the cheapest camara available from angekis",
|
| 243 |
+
"Can you find me Samsung outdoor tv-s?"
|
| 244 |
+
]
|
| 245 |
+
|
| 246 |
+
print("🚀 Running demo conversations...")
|
| 247 |
+
|
| 248 |
+
for query in test_queries:
|
| 249 |
+
print(f"\n👤 Demo Query: {query}")
|
| 250 |
+
response = run_conversation_turn(compiled_graph, thread_id, query, langsmith_client)
|
| 251 |
+
print(f"🤖 Response: {response}")
|
| 252 |
+
|
| 253 |
+
# Show conversation history
|
| 254 |
+
print("\n📝 Conversation History:")
|
| 255 |
+
history = get_conversation_history(compiled_graph, thread_id)
|
| 256 |
+
for i, msg in enumerate(history, 1):
|
| 257 |
+
print(f"{i}. [{msg['type']}]: {msg['content'][:100]}...")
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
if __name__ == "__main__":
|
| 261 |
+
# Run interactive loop
|
| 262 |
+
run_interactive_loop()
|
src/sales_assistant/agent_main/tools_node.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tools node that are using the agent_tools folder's tools
|
| 3 |
+
"""
|
| 4 |
+
from typing import List
|
| 5 |
+
from langchain_core.tools import BaseTool
|
| 6 |
+
from langgraph.prebuilt import ToolNode
|
| 7 |
+
|
| 8 |
+
# Import all the tools from agent_tools
|
| 9 |
+
from ..agent_tools.describe_table import describe_table
|
| 10 |
+
from ..agent_tools.execute_advanced_query import execute_advanced_query
|
| 11 |
+
from ..agent_tools.get_distinct_values import get_distinct_values
|
| 12 |
+
from ..agent_tools.get_product_by_criteria import search_products_by_criteria
|
| 13 |
+
from ..agent_tools.get_samples_data import get_sample_data
|
| 14 |
+
from ..agent_tools.get_table_statistics import get_table_statistics
|
| 15 |
+
from ..agent_tools.get_exchange_rates import exchange_converter
|
| 16 |
+
from ..agent_tools.execute_sql_query import execute_sql_query
|
| 17 |
+
from ..agent_tools.create_quote import create_quote
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_all_tools() -> List[BaseTool]:
|
| 21 |
+
"""
|
| 22 |
+
Get all available tools for the agent.
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
List of all available tools
|
| 26 |
+
"""
|
| 27 |
+
simple_tool_list = [execute_sql_query, exchange_converter, create_quote]
|
| 28 |
+
advanced_tool_list = [
|
| 29 |
+
describe_table,
|
| 30 |
+
execute_advanced_query,
|
| 31 |
+
get_distinct_values,
|
| 32 |
+
search_products_by_criteria,
|
| 33 |
+
get_sample_data,
|
| 34 |
+
get_table_statistics,
|
| 35 |
+
exchange_converter,
|
| 36 |
+
create_quote
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
return simple_tool_list
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# Create the tool node using LangGraph's built-in ToolNode
|
| 43 |
+
tool_node = ToolNode(get_all_tools())
|
src/sales_assistant/agent_tools/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Sales Assistant Package
|
src/sales_assistant/agent_tools/agent_tools_utils.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
Functions that help create and run the agent with its tools.
|
| 3 |
+
'''
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import logging
|
| 6 |
+
from sales_assistant.db.utils.db_utils import read_sql
|
| 7 |
+
|
| 8 |
+
# Configure logging for SQL queries
|
| 9 |
+
logging.basicConfig(level=logging.INFO)
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
def get_data_for_agent(query: str, return_type: str = "dict_records") -> str:
|
| 13 |
+
"""
|
| 14 |
+
Helper function to get data from the database for the agent.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
query (str): SQL query string to execute.
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
str: Formatted string of query results or error message.
|
| 21 |
+
"""
|
| 22 |
+
try:
|
| 23 |
+
# Log the SQL query being executed
|
| 24 |
+
print(f"🔍 SQL Query: {query}")
|
| 25 |
+
logger.info(f"Executing SQL query: {query}")
|
| 26 |
+
|
| 27 |
+
df_original = read_sql(query)
|
| 28 |
+
|
| 29 |
+
if return_type == "dict_records":
|
| 30 |
+
df_formated = df_original.to_dict(orient="records")
|
| 31 |
+
|
| 32 |
+
if return_type == "dict":
|
| 33 |
+
df_formated = df_original.to_dict()
|
| 34 |
+
|
| 35 |
+
if return_type == "string":
|
| 36 |
+
if df_original.empty:
|
| 37 |
+
df_formated = "No data found."
|
| 38 |
+
else:
|
| 39 |
+
df_formated = df_original.to_string(index=False)
|
| 40 |
+
|
| 41 |
+
return df_original, df_formated
|
| 42 |
+
|
| 43 |
+
except Exception as e:
|
| 44 |
+
data_value = f"Error retrieving data: {str(e)}"
|
| 45 |
+
df_original = pd.DataFrame()
|
| 46 |
+
df_formated = pd.DataFrame()
|
| 47 |
+
return df_original, df_formated
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def create_results_metadata(results_orignal: pd.DataFrame) -> dict:
|
| 51 |
+
"""
|
| 52 |
+
Create metadata about the results such as count of records and columns.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
results_orignal (pd.DataFrame): Original DataFrame of query results.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
dict: Metadata including counts of records and columns.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
shape = results_orignal.shape
|
| 62 |
+
rows = shape[0]
|
| 63 |
+
cols = shape[1]
|
| 64 |
+
|
| 65 |
+
metadata = {
|
| 66 |
+
'row count': rows
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
return metadata
|
src/sales_assistant/agent_tools/create_quote.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quote creation tool that generates professional quotes in Markdown format using product IDs from database.
|
| 3 |
+
This tool queries products by ID and creates structured quotes with proper Markdown tables.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from typing import Any, Dict, List, Optional, Union
|
| 9 |
+
from collections import Counter
|
| 10 |
+
from langchain_core.tools import tool
|
| 11 |
+
from langchain_openai import ChatOpenAI
|
| 12 |
+
from langchain_core.messages import HumanMessage
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
|
| 15 |
+
from .quote_template import (
|
| 16 |
+
Quote, QuoteItem, CustomerInfo,
|
| 17 |
+
generate_markdown_quote,
|
| 18 |
+
GREETING_PROMPT, INTRO_PROMPT,
|
| 19 |
+
generate_product_description
|
| 20 |
+
)
|
| 21 |
+
from .get_exchange_rates import convert_amount
|
| 22 |
+
from .agent_tools_utils import get_data_for_agent
|
| 23 |
+
|
| 24 |
+
# Load environment variables
|
| 25 |
+
load_dotenv()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def fetch_products_by_ids(product_ids: List[int], target_currency: str = "EUR") -> List[Dict[str, Any]]:
|
| 29 |
+
"""
|
| 30 |
+
Fetch product details from database by product IDs and convert prices to target currency.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
product_ids: List of product IDs to fetch
|
| 34 |
+
target_currency: Currency to convert prices to
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
List of product dictionaries with converted prices
|
| 38 |
+
"""
|
| 39 |
+
try:
|
| 40 |
+
# Query to get product details including model numbers
|
| 41 |
+
product_ids_str = ",".join(map(str, product_ids))
|
| 42 |
+
query = f"""
|
| 43 |
+
SELECT id, manufacturer, model_name, model_number_long, model_number_short,
|
| 44 |
+
description, msrp, currency, category, sub_category
|
| 45 |
+
FROM streamnet.products_list
|
| 46 |
+
WHERE id IN ({product_ids_str})
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
# Execute query
|
| 50 |
+
df_original, df_formatted = get_data_for_agent(query, return_type="dict_records")
|
| 51 |
+
|
| 52 |
+
if not df_formatted or len(df_formatted) == 0:
|
| 53 |
+
return []
|
| 54 |
+
|
| 55 |
+
# Process and convert currencies
|
| 56 |
+
products = []
|
| 57 |
+
for product in df_formatted:
|
| 58 |
+
original_currency = product.get('currency', 'EUR')
|
| 59 |
+
msrp_price = float(product.get('msrp', 0))
|
| 60 |
+
|
| 61 |
+
# Convert currency if needed
|
| 62 |
+
if original_currency != target_currency and msrp_price > 0:
|
| 63 |
+
conversion_result = convert_amount(
|
| 64 |
+
original_currency,
|
| 65 |
+
target_currency,
|
| 66 |
+
msrp_price
|
| 67 |
+
)
|
| 68 |
+
if "error" not in conversion_result:
|
| 69 |
+
converted_price = conversion_result["converted_amount"]
|
| 70 |
+
else:
|
| 71 |
+
print(f"Warning: Currency conversion failed for product {product.get('id')}: {conversion_result.get('error')}")
|
| 72 |
+
converted_price = msrp_price # Use original price
|
| 73 |
+
else:
|
| 74 |
+
converted_price = msrp_price
|
| 75 |
+
|
| 76 |
+
# Build product name
|
| 77 |
+
product_name = product.get('model_name', '') or product.get('model_number_long', '') or f"Product {product.get('id')}"
|
| 78 |
+
|
| 79 |
+
products.append({
|
| 80 |
+
'id': product.get('id'),
|
| 81 |
+
'product_name': product_name,
|
| 82 |
+
'model_number_short': product.get('model_number_short'),
|
| 83 |
+
'model_number_long': product.get('model_number_long'),
|
| 84 |
+
'unit_price': converted_price,
|
| 85 |
+
'currency': target_currency,
|
| 86 |
+
'original_currency': original_currency,
|
| 87 |
+
'manufacturer': product.get('manufacturer', ''),
|
| 88 |
+
'category': product.get('category', ''),
|
| 89 |
+
'sub_category': product.get('sub_category', ''),
|
| 90 |
+
'original_description': product.get('description', '')
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
return products
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"Error fetching products: {e}")
|
| 97 |
+
return []
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def generate_dynamic_content(prompt: str, max_retries: int = 2) -> str:
|
| 101 |
+
"""
|
| 102 |
+
Generate dynamic content using LLM.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
prompt: The prompt for content generation
|
| 106 |
+
max_retries: Number of retry attempts if generation fails
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Generated content string
|
| 110 |
+
"""
|
| 111 |
+
try:
|
| 112 |
+
# Initialize the LLM
|
| 113 |
+
model = ChatOpenAI(
|
| 114 |
+
model=os.getenv("MODEL_NAME", "gpt-4o-mini"),
|
| 115 |
+
api_key=os.getenv("OPENAI_API_KEY"),
|
| 116 |
+
temperature=0.3 # Slightly creative but controlled
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Generate content
|
| 120 |
+
response = model.invoke([HumanMessage(content=prompt)])
|
| 121 |
+
return response.content.strip()
|
| 122 |
+
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print(f"Warning: LLM content generation failed: {e}")
|
| 125 |
+
# Fallback to generic content
|
| 126 |
+
if "greeting" in prompt.lower():
|
| 127 |
+
return "Dear Valued Customer, thank you for your interest in our products."
|
| 128 |
+
else:
|
| 129 |
+
return "We are pleased to provide you with this detailed quotation for your consideration."
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def create_quote_file(quote: Quote, content: str) -> str:
|
| 133 |
+
"""
|
| 134 |
+
Save the quote to a Markdown file in the created_quotes directory.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
quote: Quote object with metadata
|
| 138 |
+
content: The formatted quote content
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
Path to the saved file
|
| 142 |
+
"""
|
| 143 |
+
# Ensure the created_quotes directory exists
|
| 144 |
+
quotes_dir = "/Users/krinya/sales_assistant_with_quote/src/sales_assistant/created_quotes"
|
| 145 |
+
os.makedirs(quotes_dir, exist_ok=True)
|
| 146 |
+
|
| 147 |
+
# Generate unique filename
|
| 148 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 149 |
+
customer_name = quote.customer.name.replace(" ", "_").replace("/", "_")
|
| 150 |
+
filename = f"quote_{quote.quote_id}_{customer_name}_{timestamp}.md"
|
| 151 |
+
filepath = os.path.join(quotes_dir, filename)
|
| 152 |
+
|
| 153 |
+
# Write the quote to file
|
| 154 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 155 |
+
f.write(content)
|
| 156 |
+
|
| 157 |
+
return filepath
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@tool
|
| 161 |
+
def create_quote(
|
| 162 |
+
product_ids: List[int],
|
| 163 |
+
customer_name: str,
|
| 164 |
+
customer_email: Optional[str] = None,
|
| 165 |
+
customer_company: Optional[str] = None,
|
| 166 |
+
target_currency: str = "EUR",
|
| 167 |
+
notes: Optional[str] = None
|
| 168 |
+
) -> Dict[str, Any]:
|
| 169 |
+
"""
|
| 170 |
+
A quote creation tool that generates quotes using product IDs from the database.
|
| 171 |
+
|
| 172 |
+
This tool takes a list of product IDs (can include duplicates for multiple quantities),
|
| 173 |
+
fetches product details from the database, and generates a professional quote with
|
| 174 |
+
proper Markdown tables and formatting. This can only works if the products have prices
|
| 175 |
+
set in the database.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
product_ids (List[int]): List of product IDs from database. Duplicates indicate multiple quantities. Example: [1, 1, 1, 3, 4, 5] means 3x product ID 1, 1x product ID 3, 1x product ID 4, 1x product ID 5
|
| 179 |
+
customer_name (str): Customer's full name (required)
|
| 180 |
+
customer_email (Optional[str]): Customer's email address
|
| 181 |
+
customer_company (Optional[str]): Customer's company name
|
| 182 |
+
target_currency (str): Currency for the final quote (EUR, USD, HUF)
|
| 183 |
+
notes (Optional[str]): Additional notes for the quote
|
| 184 |
+
|
| 185 |
+
Example usage:
|
| 186 |
+
create_quote(
|
| 187 |
+
product_ids=[123, 123, 456, 789], # 2x product 123, 1x product 456, 1x product 789
|
| 188 |
+
customer_name="John Smith",
|
| 189 |
+
customer_email="john@company.com",
|
| 190 |
+
customer_company="Tech Solutions Inc",
|
| 191 |
+
target_currency="EUR"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
Returns:
|
| 195 |
+
Dict containing quote details and file path.
|
| 196 |
+
"""
|
| 197 |
+
try:
|
| 198 |
+
# Validate inputs
|
| 199 |
+
if not customer_name:
|
| 200 |
+
return {"error": "Customer name is required"}
|
| 201 |
+
|
| 202 |
+
if not product_ids or len(product_ids) == 0:
|
| 203 |
+
return {"error": "At least one product ID is required"}
|
| 204 |
+
|
| 205 |
+
# Count quantities for each product ID
|
| 206 |
+
product_quantities = Counter(product_ids)
|
| 207 |
+
unique_product_ids = list(product_quantities.keys())
|
| 208 |
+
|
| 209 |
+
# Fetch product details from database
|
| 210 |
+
products_data = fetch_products_by_ids(unique_product_ids, target_currency)
|
| 211 |
+
|
| 212 |
+
if not products_data:
|
| 213 |
+
return {"error": "No products found for the provided IDs"}
|
| 214 |
+
|
| 215 |
+
# Create customer info
|
| 216 |
+
customer = CustomerInfo(
|
| 217 |
+
name=customer_name,
|
| 218 |
+
email=customer_email,
|
| 219 |
+
company=customer_company
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
# Initialize LLM for description generation
|
| 223 |
+
llm = ChatOpenAI(
|
| 224 |
+
model=os.getenv("MODEL_NAME", "gpt-4o-mini"),
|
| 225 |
+
api_key=os.getenv("OPENAI_API_KEY"),
|
| 226 |
+
temperature=0.3
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# Create quote items with quantities and LLM-generated descriptions
|
| 230 |
+
quote_items = []
|
| 231 |
+
for product_data in products_data:
|
| 232 |
+
product_id = product_data['id']
|
| 233 |
+
quantity = product_quantities[product_id]
|
| 234 |
+
|
| 235 |
+
# Generate concise description using LLM
|
| 236 |
+
product_info = {
|
| 237 |
+
'manufacturer': product_data['manufacturer'],
|
| 238 |
+
'model_name': product_data['product_name'],
|
| 239 |
+
'category': product_data['category'],
|
| 240 |
+
'sub_category': product_data['sub_category'],
|
| 241 |
+
'description': product_data['original_description']
|
| 242 |
+
}
|
| 243 |
+
llm_description = generate_product_description(product_info, llm)
|
| 244 |
+
|
| 245 |
+
quote_item = QuoteItem(
|
| 246 |
+
product_id=product_id,
|
| 247 |
+
product_name=product_data['product_name'],
|
| 248 |
+
model_number_short=product_data['model_number_short'],
|
| 249 |
+
model_number_long=product_data['model_number_long'],
|
| 250 |
+
description=llm_description,
|
| 251 |
+
quantity=quantity,
|
| 252 |
+
unit_price=product_data['unit_price'],
|
| 253 |
+
currency=target_currency
|
| 254 |
+
)
|
| 255 |
+
quote_items.append(quote_item)
|
| 256 |
+
|
| 257 |
+
# Create the quote object
|
| 258 |
+
quote = Quote(
|
| 259 |
+
customer=customer,
|
| 260 |
+
items=quote_items,
|
| 261 |
+
currency=target_currency,
|
| 262 |
+
notes=notes
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
# Generate LLM content
|
| 266 |
+
company_info = f" from {customer_company}" if customer_company else ""
|
| 267 |
+
|
| 268 |
+
# Generate greeting
|
| 269 |
+
greeting_prompt = GREETING_PROMPT.format(
|
| 270 |
+
customer_name=customer_name,
|
| 271 |
+
company_info=company_info,
|
| 272 |
+
company=customer_company or "N/A"
|
| 273 |
+
)
|
| 274 |
+
greeting = generate_dynamic_content(greeting_prompt)
|
| 275 |
+
|
| 276 |
+
# Generate introduction
|
| 277 |
+
product_summary = ", ".join([item.product_name for item in quote_items[:3]])
|
| 278 |
+
if len(quote_items) > 3:
|
| 279 |
+
product_summary += f" and {len(quote_items) - 3} other item(s)"
|
| 280 |
+
|
| 281 |
+
intro_prompt = INTRO_PROMPT.format(
|
| 282 |
+
customer_name=customer_name,
|
| 283 |
+
company=customer_company or "your organization",
|
| 284 |
+
product_summary=product_summary,
|
| 285 |
+
item_count=len(quote_items)
|
| 286 |
+
)
|
| 287 |
+
introduction = generate_dynamic_content(intro_prompt)
|
| 288 |
+
|
| 289 |
+
# Generate the complete Markdown quote
|
| 290 |
+
quote_template = generate_markdown_quote(quote)
|
| 291 |
+
final_quote = quote_template.format(
|
| 292 |
+
greeting=greeting,
|
| 293 |
+
introduction=introduction
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Save to Markdown file
|
| 297 |
+
file_path = create_quote_file(quote, final_quote)
|
| 298 |
+
|
| 299 |
+
# Prepare response
|
| 300 |
+
response = {
|
| 301 |
+
"success": True,
|
| 302 |
+
"quote_id": quote.quote_id,
|
| 303 |
+
"customer_name": customer_name,
|
| 304 |
+
"grand_total": quote.grand_total,
|
| 305 |
+
"currency": target_currency,
|
| 306 |
+
"item_count": len(quote_items),
|
| 307 |
+
"unique_products": len(unique_product_ids),
|
| 308 |
+
"total_items": sum(product_quantities.values()),
|
| 309 |
+
"file_path": file_path,
|
| 310 |
+
"file_format": "markdown",
|
| 311 |
+
"created_date": quote.created_date.isoformat(),
|
| 312 |
+
"valid_until": quote.valid_until.isoformat(),
|
| 313 |
+
"quote_summary": {
|
| 314 |
+
"grand_total": quote.grand_total,
|
| 315 |
+
"items": [
|
| 316 |
+
{
|
| 317 |
+
"product_id": item.product_id,
|
| 318 |
+
"name": item.product_name,
|
| 319 |
+
"quantity": item.quantity,
|
| 320 |
+
"unit_price": item.unit_price,
|
| 321 |
+
"total": item.total_price
|
| 322 |
+
} for item in quote_items
|
| 323 |
+
]
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
return response
|
| 328 |
+
|
| 329 |
+
except Exception as e:
|
| 330 |
+
return {"error": f"Quote creation failed: {str(e)}"}
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def create_sample_quote_from_ids():
|
| 334 |
+
"""Create a sample quote using product IDs for testing purposes."""
|
| 335 |
+
|
| 336 |
+
# Sample with some repeated IDs to test quantity handling
|
| 337 |
+
sample_product_ids = [760, 760, 763, 765, 765, 768] # 2x product 760, 1x product 763, 2x product 765, 1x product 768
|
| 338 |
+
|
| 339 |
+
result = create_quote.invoke({
|
| 340 |
+
"product_ids": sample_product_ids,
|
| 341 |
+
"customer_name": "Jane Doe",
|
| 342 |
+
"customer_email": "jane.doe@example.com",
|
| 343 |
+
"customer_company": "Example Corp",
|
| 344 |
+
"target_currency": "EUR",
|
| 345 |
+
"notes": "Sample quote generated from product IDs"
|
| 346 |
+
})
|
| 347 |
+
|
| 348 |
+
return result
|
src/sales_assistant/agent_tools/describe_table.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from .agent_tools_utils import *
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@tool
|
| 8 |
+
def describe_table(sql_query: str = "DESCRIBE streamnet.products_list;") -> Dict[str, Any]:
|
| 9 |
+
"""
|
| 10 |
+
Explore the schema of a database table using an SQL query.
|
| 11 |
+
You can modify the query to explore different tables or get specific schema information.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
sql_query (str): SQL query to explore table schema.
|
| 15 |
+
Examples:
|
| 16 |
+
- "DESCRIBE streamnet.products_list;"
|
| 17 |
+
- "SHOW COLUMNS FROM streamnet.products_list;"
|
| 18 |
+
- "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_NAME = 'products_list';"
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
Dict[str, Any]: Table schema information from the query results.
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
results_orignal, results_formated = get_data_for_agent(sql_query, return_type="dict_records")
|
| 25 |
+
metadata = create_results_metadata(results_orignal)
|
| 26 |
+
|
| 27 |
+
result_dict = {
|
| 28 |
+
"query_executed": sql_query,
|
| 29 |
+
"data_from_db": results_formated,
|
| 30 |
+
"metadata": metadata
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return result_dict
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
return {"error": f"Schema exploration failed: {str(e)}"}
|
src/sales_assistant/agent_tools/execute_advanced_query.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
from .agent_tools_utils import *
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@tool
|
| 7 |
+
def execute_advanced_query(sql_query: str) -> Dict[str, Any]:
|
| 8 |
+
"""
|
| 9 |
+
Execute any advanced SQL query for complex data exploration, joins, subqueries, and analytical operations.
|
| 10 |
+
This tool gives you complete flexibility to run sophisticated database operations.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
sql_query (str): Any valid SQL query for advanced operations.
|
| 14 |
+
|
| 15 |
+
Examples:
|
| 16 |
+
- "SELECT manufacturer, category, AVG(msrp) as avg_price FROM streamnet.products_list WHERE msrp > 0 GROUP BY manufacturer, category HAVING COUNT(*) > 5 ORDER BY avg_price DESC;"
|
| 17 |
+
- "SELECT * FROM streamnet.products_list WHERE msrp = (SELECT MAX(msrp) FROM streamnet.products_list WHERE manufacturer = 'Apple');"
|
| 18 |
+
- "SELECT manufacturer, COUNT(*) as products, ROUND(AVG(msrp), 2) as avg_price, ROUND(STDDEV(msrp), 2) as price_std FROM streamnet.products_list GROUP BY manufacturer HAVING COUNT(*) > 10;"
|
| 19 |
+
- "SELECT YEAR(NOW()) as current_year, manufacturer, model_name, msrp FROM streamnet.products_list WHERE description LIKE '%2024%' OR description LIKE '%new%';"
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
Dict[str, Any]: Results from the advanced query execution.
|
| 23 |
+
"""
|
| 24 |
+
try:
|
| 25 |
+
results_orignal, results_formated = get_data_for_agent(sql_query, return_type="dict_records")
|
| 26 |
+
metadata = create_results_metadata(results_orignal)
|
| 27 |
+
|
| 28 |
+
result_dict = {
|
| 29 |
+
"query_executed": sql_query,
|
| 30 |
+
"data_from_db": results_formated,
|
| 31 |
+
"metadata": metadata
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return result_dict
|
| 35 |
+
|
| 36 |
+
except Exception as e:
|
| 37 |
+
return {"error": f"Advanced query execution failed: {str(e)}"}
|
src/sales_assistant/agent_tools/execute_sql_query.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Optional, Literal
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
from .agent_tools_utils import *
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@tool
|
| 7 |
+
def execute_sql_query(
|
| 8 |
+
sql_query: str,
|
| 9 |
+
query_type: Optional[Literal["search", "statistics", "distinct_values", "sample", "advanced"]] = None
|
| 10 |
+
) -> Dict[str, Any]:
|
| 11 |
+
"""
|
| 12 |
+
Execute any SQL query against the database with complete flexibility for all types of operations.
|
| 13 |
+
This unified tool handles product searches, statistical analysis, data exploration, sampling, and advanced queries.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
sql_query (str): Any valid SQL query to execute against the streamnet.products_list table.
|
| 17 |
+
query_type (Optional[str]): Hint about the type of query for better organization. Options:
|
| 18 |
+
- "search": Product searches with filtering and criteria
|
| 19 |
+
- "statistics": Statistical analysis and aggregations
|
| 20 |
+
- "distinct_values": Exploring unique values and patterns
|
| 21 |
+
- "sample": Data sampling and exploration
|
| 22 |
+
- "advanced": Complex queries with joins, subqueries, analytics
|
| 23 |
+
|
| 24 |
+
Query Examples by Type:
|
| 25 |
+
|
| 26 |
+
SEARCH QUERIES:
|
| 27 |
+
- "SELECT * FROM streamnet.products_list WHERE manufacturer = 'Samsung' AND category = 'Electronics' LIMIT 15;"
|
| 28 |
+
- "SELECT * FROM streamnet.products_list WHERE description LIKE '%wireless%' AND msrp BETWEEN 50 AND 200;"
|
| 29 |
+
- "SELECT * FROM streamnet.products_list WHERE model_name LIKE '%Pro%' ORDER BY msrp DESC LIMIT 10;"
|
| 30 |
+
- "SELECT manufacturer, model_name, msrp FROM streamnet.products_list WHERE sub_category IN ('Smartphones', 'Tablets') ORDER BY msrp;"
|
| 31 |
+
|
| 32 |
+
STATISTICS QUERIES:
|
| 33 |
+
- "SELECT COUNT(*) as total_products, AVG(msrp) as avg_price, MIN(msrp) as min_price, MAX(msrp) as max_price FROM streamnet.products_list;"
|
| 34 |
+
- "SELECT manufacturer, COUNT(*) as product_count, AVG(msrp) as avg_price FROM streamnet.products_list GROUP BY manufacturer ORDER BY product_count DESC;"
|
| 35 |
+
- "SELECT category, sub_category, COUNT(*) as count FROM streamnet.products_list GROUP BY category, sub_category ORDER BY count DESC;"
|
| 36 |
+
- "SELECT currency, COUNT(*) as products, MIN(msrp) as min_price, MAX(msrp) as max_price FROM streamnet.products_list GROUP BY currency;"
|
| 37 |
+
|
| 38 |
+
DISTINCT VALUES QUERIES:
|
| 39 |
+
- "SELECT manufacturer, COUNT(*) as count FROM streamnet.products_list GROUP BY manufacturer ORDER BY count DESC LIMIT 20;"
|
| 40 |
+
- "SELECT category, COUNT(*) as count FROM streamnet.products_list GROUP BY category ORDER BY count DESC;"
|
| 41 |
+
- "SELECT manufacturer, sub_category, COUNT(*) as count FROM streamnet.products_list WHERE manufacturer = 'Samsung' GROUP BY manufacturer, sub_category;"
|
| 42 |
+
- "SELECT DISTINCT model_name FROM streamnet.products_list WHERE category = 'Electronics' ORDER BY model_name;"
|
| 43 |
+
- "SELECT currency, AVG(msrp) as avg_price, COUNT(*) as product_count FROM streamnet.products_list GROUP BY currency;"
|
| 44 |
+
|
| 45 |
+
SAMPLE QUERIES:
|
| 46 |
+
- "SELECT * FROM streamnet.products_list LIMIT 20;"
|
| 47 |
+
- "SELECT * FROM streamnet.products_list WHERE manufacturer = 'Samsung' ORDER BY RAND() LIMIT 15;"
|
| 48 |
+
- "SELECT * FROM streamnet.products_list WHERE category = 'camera' AND sub_category LIKE '%accessories%' LIMIT 10;"
|
| 49 |
+
- "SELECT manufacturer, category, COUNT(*) FROM streamnet.products_list GROUP BY manufacturer, category LIMIT 25;"
|
| 50 |
+
- "SELECT * FROM streamnet.products_list WHERE msrp > 100 ORDER BY RAND() LIMIT 30;"
|
| 51 |
+
|
| 52 |
+
ADVANCED QUERIES:
|
| 53 |
+
- "SELECT manufacturer, category, AVG(msrp) as avg_price FROM streamnet.products_list WHERE msrp > 0 GROUP BY manufacturer, category HAVING COUNT(*) > 5 ORDER BY avg_price DESC;"
|
| 54 |
+
- "SELECT * FROM streamnet.products_list WHERE msrp = (SELECT MAX(msrp) FROM streamnet.products_list WHERE manufacturer = 'Apple');"
|
| 55 |
+
- "SELECT manufacturer, COUNT(*) as products, ROUND(AVG(msrp), 2) as avg_price, ROUND(STDDEV(msrp), 2) as price_std FROM streamnet.products_list GROUP BY manufacturer HAVING COUNT(*) > 10;"
|
| 56 |
+
- "SELECT YEAR(NOW()) as current_year, manufacturer, model_name, msrp FROM streamnet.products_list WHERE description LIKE '%2024%' OR description LIKE '%new%';"
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Dict[str, Any]: Results from the SQL query execution with metadata.
|
| 60 |
+
"""
|
| 61 |
+
try:
|
| 62 |
+
results_original, results_formatted = get_data_for_agent(sql_query, return_type="dict_records")
|
| 63 |
+
metadata = create_results_metadata(results_original)
|
| 64 |
+
|
| 65 |
+
result_dict = {
|
| 66 |
+
"query_executed": sql_query,
|
| 67 |
+
"data_from_db": results_formatted,
|
| 68 |
+
"query_type": query_type,
|
| 69 |
+
"metadata": metadata
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return result_dict
|
| 73 |
+
|
| 74 |
+
except Exception as e:
|
| 75 |
+
return {
|
| 76 |
+
"error": f"SQL query execution failed: {str(e)}",
|
| 77 |
+
"query_executed": sql_query,
|
| 78 |
+
"query_type": query_type
|
| 79 |
+
}
|
src/sales_assistant/agent_tools/get_distinct_values.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
from .agent_tools_utils import *
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@tool
|
| 7 |
+
def get_distinct_values(sql_query: str = "SELECT manufacturer, COUNT(*) as count FROM streamnet.products_list GROUP BY manufacturer ORDER BY count DESC LIMIT 20;") -> Dict[str, Any]:
|
| 8 |
+
"""
|
| 9 |
+
Explore distinct values and patterns using custom SQL queries with grouping, filtering, and aggregation.
|
| 10 |
+
You can dynamically modify this to explore any column combinations, apply filters, and get insights.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
sql_query (str): SQL query to explore distinct values and patterns.
|
| 14 |
+
|
| 15 |
+
Examples:
|
| 16 |
+
- "SELECT category, COUNT(*) as count FROM streamnet.products_list GROUP BY category ORDER BY count DESC;"
|
| 17 |
+
- "SELECT manufacturer, sub_category, COUNT(*) as count FROM streamnet.products_list WHERE manufacturer = 'Samsung' GROUP BY manufacturer, sub_category;"
|
| 18 |
+
- "SELECT DISTINCT model_name FROM streamnet.products_list WHERE category = 'Electronics' ORDER BY model_name;"
|
| 19 |
+
- "SELECT currency, AVG(msrp) as avg_price, COUNT(*) as product_count FROM streamnet.products_list GROUP BY currency;"
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
Dict[str, Any]: Distinct values with their counts and patterns.
|
| 23 |
+
"""
|
| 24 |
+
try:
|
| 25 |
+
results_orignal, results_formated = get_data_for_agent(sql_query, return_type="dict_records")
|
| 26 |
+
metadata = create_results_metadata(results_orignal)
|
| 27 |
+
|
| 28 |
+
result_dict = {
|
| 29 |
+
"query_executed": sql_query,
|
| 30 |
+
"data_from_db": results_formated,
|
| 31 |
+
"metadata": metadata
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return result_dict
|
| 35 |
+
|
| 36 |
+
except Exception as e:
|
| 37 |
+
return {"error": f"Distinct values exploration failed: {str(e)}"}
|
src/sales_assistant/agent_tools/get_exchange_rates.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
|
| 4 |
+
# Fixed exchange rates relative to HUF (kept simple and deterministic)
|
| 5 |
+
_EXCHANGE_RATES = {
|
| 6 |
+
"HUF": 1.0,
|
| 7 |
+
"EUR": 400.0,
|
| 8 |
+
"USD": 340.0
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _normalize_currency(code: str) -> str:
|
| 13 |
+
return code.strip().upper() if isinstance(code, str) else ""
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def convert_amount(from_currency: str, to_currency: str, amount: float, precision: int = 2) -> Dict[str, Any]:
|
| 17 |
+
"""Simple, deterministic currency converter.
|
| 18 |
+
|
| 19 |
+
Returns a small dict on success: {
|
| 20 |
+
"converted_amount": float,
|
| 21 |
+
"exchange_rate": float, # target per one unit of source
|
| 22 |
+
"from": str,
|
| 23 |
+
"to": str
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
On error returns {"error": "message"}.
|
| 27 |
+
"""
|
| 28 |
+
fc = _normalize_currency(from_currency)
|
| 29 |
+
tc = _normalize_currency(to_currency)
|
| 30 |
+
|
| 31 |
+
if not fc or not tc:
|
| 32 |
+
return {"error": "from_currency and to_currency are required strings"}
|
| 33 |
+
|
| 34 |
+
if fc not in _EXCHANGE_RATES or tc not in _EXCHANGE_RATES:
|
| 35 |
+
return {"error": f"Unsupported currency. Supported: {', '.join(sorted(_EXCHANGE_RATES.keys()))}"}
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
amt = float(amount)
|
| 39 |
+
except Exception:
|
| 40 |
+
return {"error": "Amount must be a number"}
|
| 41 |
+
|
| 42 |
+
if amt < 0:
|
| 43 |
+
return {"error": "Amount must be non-negative"}
|
| 44 |
+
|
| 45 |
+
if fc == tc:
|
| 46 |
+
return {"converted_amount": round(amt, precision), "exchange_rate": 1.0, "from": fc, "to": tc}
|
| 47 |
+
|
| 48 |
+
# Convert via HUF as base: source -> HUF -> target
|
| 49 |
+
converted = (amt * _EXCHANGE_RATES[fc]) / _EXCHANGE_RATES[tc]
|
| 50 |
+
rate = converted / amt if amt != 0 else 0.0
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
"original_amount": round(amt, precision),
|
| 54 |
+
"from": fc,
|
| 55 |
+
"converted_amount": round(converted, precision),
|
| 56 |
+
"to": tc,
|
| 57 |
+
"exchange_rate": round(rate, max(4, precision))
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@tool
|
| 62 |
+
def exchange_converter(from_currency: str = "EUR", to_currency: str = "HUF", amount: float = 1.0, precision: int = 2) -> Dict[str, Any]:
|
| 63 |
+
"""
|
| 64 |
+
Currency converter tool. That can convert between EUR, USD, and HUF, allowing specification of precision.
|
| 65 |
+
"""
|
| 66 |
+
return convert_amount(from_currency, to_currency, amount, precision)
|
src/sales_assistant/agent_tools/get_product_by_criteria.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
from .agent_tools_utils import *
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@tool
|
| 7 |
+
def search_products_by_criteria(sql_query: str = "SELECT * FROM streamnet.products_list WHERE manufacturer LIKE '%Samsung%' LIMIT 10;") -> Dict[str, Any]:
|
| 8 |
+
"""
|
| 9 |
+
Search for products using dynamic SQL queries with complex filtering, sorting, and conditions.
|
| 10 |
+
You can build sophisticated searches with multiple criteria, price ranges, text matching, and more.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
sql_query (str): SQL query to search for products with custom criteria.
|
| 14 |
+
Examples:
|
| 15 |
+
- "SELECT * FROM streamnet.products_list WHERE manufacturer = 'Samsung' AND category = 'Electronics' LIMIT 15;"
|
| 16 |
+
- "SELECT * FROM streamnet.products_list WHERE description LIKE '%wireless%' AND msrp BETWEEN 50 AND 200;"
|
| 17 |
+
- "SELECT * FROM streamnet.products_list WHERE model_name LIKE '%Pro%' ORDER BY msrp DESC LIMIT 10;"
|
| 18 |
+
- "SELECT manufacturer, model_name, msrp FROM streamnet.products_list WHERE sub_category IN ('Smartphones', 'Tablets') ORDER BY msrp;"
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
Dict[str, Any]: Matching products based on search criteria.
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
results_orignal, results_formated = get_data_for_agent(sql_query, return_type="dict_records")
|
| 25 |
+
results_metadata = create_results_metadata(results_orignal)
|
| 26 |
+
|
| 27 |
+
result_dict = {
|
| 28 |
+
"query_executed": sql_query,
|
| 29 |
+
"data_from_db": results_formated,
|
| 30 |
+
"metadata": results_metadata
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return result_dict
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
return {"error": f"Product search failed: {str(e)}"}
|
src/sales_assistant/agent_tools/get_samples_data.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, Optional
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from .agent_tools_utils import *
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@tool
|
| 8 |
+
def get_sample_data(sql_query: str = "SELECT * FROM streamnet.products_list", method: Optional[str] = "limit", sample_size: Optional[int] = 20) -> Dict[str, Any]:
|
| 9 |
+
"""
|
| 10 |
+
Get sample data using a custom SQL query with different sampling methods.
|
| 11 |
+
You can modify this query to explore specific data patterns, filter by conditions, or sample different subsets.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
sql_query (str): Base SQL query to retrieve data (without LIMIT or ORDER BY RAND()).
|
| 15 |
+
Examples:
|
| 16 |
+
- "SELECT * FROM streamnet.products_list"
|
| 17 |
+
- "SELECT * FROM streamnet.products_list WHERE manufacturer = 'Samsung'"
|
| 18 |
+
- "SELECT * FROM streamnet.products_list WHERE category = 'camera' AND sub_category LIKE '%accessories%'"
|
| 19 |
+
- "SELECT manufacturer, category, COUNT(*) FROM streamnet.products_list GROUP BY manufacturer, category"
|
| 20 |
+
- "SELECT * FROM streamnet.products_list WHERE msrp > 100"
|
| 21 |
+
|
| 22 |
+
method (Optional[str]): Sampling method to use. Options:
|
| 23 |
+
- "limit": Use LIMIT clause to get first N records
|
| 24 |
+
- "random": Use ORDER BY RAND() LIMIT to get random N records
|
| 25 |
+
- None: Return all results without limiting (ignores sample_size)
|
| 26 |
+
|
| 27 |
+
sample_size (Optional[int]): Number of sample records to retrieve (default is 20). Only used if method is "limit" or "random".
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Dict[str, Any]: Sample rows from the query execution with metadata.
|
| 31 |
+
"""
|
| 32 |
+
try:
|
| 33 |
+
# Build the final query based on the method
|
| 34 |
+
final_query = sql_query.rstrip(';') # Remove trailing semicolon if present
|
| 35 |
+
|
| 36 |
+
if method == "limit":
|
| 37 |
+
final_query += f" LIMIT {sample_size};"
|
| 38 |
+
elif method == "random":
|
| 39 |
+
final_query += f" ORDER BY RAND() LIMIT {sample_size};"
|
| 40 |
+
elif method is None or method.lower() == "none":
|
| 41 |
+
final_query += ";"
|
| 42 |
+
else:
|
| 43 |
+
result = f"Invalid value for the method arg: '{method}'. Must be 'limit', 'random', or None"
|
| 44 |
+
|
| 45 |
+
results_orignal, results_formated = get_data_for_agent(final_query, return_type="dict_records")
|
| 46 |
+
metadata = create_results_metadata(results_orignal)
|
| 47 |
+
|
| 48 |
+
result_info = {
|
| 49 |
+
"query_executed": final_query,
|
| 50 |
+
"data_from_db": results_formated,
|
| 51 |
+
"metadata": metadata
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return result_info
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return {"error": f"Data sampling failed: {str(e)}"}
|
src/sales_assistant/agent_tools/get_table_statistics.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
from .agent_tools_utils import *
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@tool
|
| 7 |
+
def get_table_statistics(sql_query: str = "SELECT COUNT(*) as total_rows, COUNT(DISTINCT manufacturer) as unique_manufacturers, COUNT(DISTINCT category) as unique_categories FROM streamnet.products_list;") -> Dict[str, Any]:
|
| 8 |
+
"""
|
| 9 |
+
Get statistics and analytical insights using custom SQL queries with aggregations.
|
| 10 |
+
You can modify this to get different statistical views, price analytics, distribution analysis, and more.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
sql_query (str): SQL query to generate statistics and insights.
|
| 14 |
+
Examples:
|
| 15 |
+
- "SELECT COUNT(*) as total_products, AVG(msrp) as avg_price, MIN(msrp) as min_price, MAX(msrp) as max_price FROM streamnet.products_list;"
|
| 16 |
+
- "SELECT manufacturer, COUNT(*) as product_count, AVG(msrp) as avg_price FROM streamnet.products_list GROUP BY manufacturer ORDER BY product_count DESC;"
|
| 17 |
+
- "SELECT category, sub_category, COUNT(*) as count FROM streamnet.products_list GROUP BY category, sub_category ORDER BY count DESC;"
|
| 18 |
+
- "SELECT currency, COUNT(*) as products, MIN(msrp) as min_price, MAX(msrp) as max_price FROM streamnet.products_list GROUP BY currency;"
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
Dict[str, Any]: Statistical results and insights.
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
results_orignal, results_formated = get_data_for_agent(sql_query, return_type="dict_records")
|
| 25 |
+
metadata = create_results_metadata(results_orignal)
|
| 26 |
+
|
| 27 |
+
result_dict = {
|
| 28 |
+
"query_executed": sql_query,
|
| 29 |
+
"data_from_db": results_formated,
|
| 30 |
+
"metadata": metadata
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return result_dict
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
return {"error": f"Statistics gathering failed: {str(e)}"}
|
src/sales_assistant/agent_tools/quote_template.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quote template structures and Pydantic models for quote generation.
|
| 3 |
+
This module contains the data models and templates used for creating professional quotes in Markdown format.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from typing import List, Optional
|
| 8 |
+
from pydantic import BaseModel, Field
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class QuoteItem(BaseModel):
|
| 13 |
+
"""Individual item in a quote with pricing details."""
|
| 14 |
+
product_id: int = Field(..., description="Database product ID")
|
| 15 |
+
product_name: str = Field(..., description="Name/model of the product")
|
| 16 |
+
model_number_short: Optional[str] = Field(None, description="Short model number")
|
| 17 |
+
model_number_long: Optional[str] = Field(None, description="Long model number")
|
| 18 |
+
description: str = Field(..., description="Detailed description of the product")
|
| 19 |
+
quantity: int = Field(..., gt=0, description="Quantity of items")
|
| 20 |
+
unit_price: float = Field(..., ge=0, description="MSRP price per unit")
|
| 21 |
+
currency: str = Field(default="EUR", description="Currency code")
|
| 22 |
+
|
| 23 |
+
@property
|
| 24 |
+
def display_product_id(self) -> str:
|
| 25 |
+
"""Get the product ID to display (prefer model_number_short, then model_number_long, then database ID)."""
|
| 26 |
+
if self.model_number_short:
|
| 27 |
+
return self.model_number_short
|
| 28 |
+
elif self.model_number_long:
|
| 29 |
+
return self.model_number_long
|
| 30 |
+
else:
|
| 31 |
+
return str(self.product_id)
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def total_price(self) -> float:
|
| 35 |
+
"""Calculate total price for this item."""
|
| 36 |
+
return self.quantity * self.unit_price
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class CustomerInfo(BaseModel):
|
| 40 |
+
"""Customer information for the quote."""
|
| 41 |
+
name: str = Field(..., description="Customer name")
|
| 42 |
+
email: Optional[str] = Field(None, description="Customer email")
|
| 43 |
+
company: Optional[str] = Field(None, description="Company name")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class Quote(BaseModel):
|
| 47 |
+
"""Complete quote with all details - no tax calculations."""
|
| 48 |
+
quote_id: str = Field(default_factory=lambda: f"QT-{uuid.uuid4().hex[:8].upper()}")
|
| 49 |
+
customer: CustomerInfo
|
| 50 |
+
items: List[QuoteItem] = Field(..., min_items=1)
|
| 51 |
+
currency: str = Field(default="EUR", description="Quote currency")
|
| 52 |
+
created_date: datetime = Field(default_factory=datetime.now)
|
| 53 |
+
valid_until: datetime = Field(default_factory=lambda: datetime.now() + timedelta(days=30))
|
| 54 |
+
notes: Optional[str] = Field(None, description="Additional notes")
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def grand_total(self) -> float:
|
| 58 |
+
"""Calculate grand total of all items."""
|
| 59 |
+
return sum(item.total_price for item in self.items)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# LLM prompts for dynamic content generation
|
| 63 |
+
GREETING_PROMPT = """
|
| 64 |
+
Generate a professional, warm greeting for a sales quote. The greeting should be 1-2 lines maximum.
|
| 65 |
+
Consider the customer information: {customer_name} {company_info}
|
| 66 |
+
|
| 67 |
+
Make it personal but professional. Examples:
|
| 68 |
+
- "Dear [Name], thank you for your interest in our products."
|
| 69 |
+
- "Hello [Name], we're pleased to provide you with the following quotation."
|
| 70 |
+
|
| 71 |
+
Customer Name: {customer_name}
|
| 72 |
+
Company: {company}
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
INTRO_PROMPT = """
|
| 76 |
+
Generate a professional introduction paragraph (2-3 sentences) for a sales quote.
|
| 77 |
+
The introduction should:
|
| 78 |
+
- Reference the products being quoted
|
| 79 |
+
- Express confidence in meeting their needs
|
| 80 |
+
- Be professional but friendly
|
| 81 |
+
- Be concise (max 3 sentences)
|
| 82 |
+
|
| 83 |
+
Customer: {customer_name}
|
| 84 |
+
Company: {company}
|
| 85 |
+
Products being quoted: {product_summary}
|
| 86 |
+
Number of items: {item_count}
|
| 87 |
+
|
| 88 |
+
Generate only the introduction paragraph, no additional formatting.
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
DESCRIPTION_GENERATION_PROMPT = """You are a professional product description writer for technology equipment quotes.
|
| 92 |
+
|
| 93 |
+
Given the following product information:
|
| 94 |
+
- Manufacturer: {manufacturer}
|
| 95 |
+
- Model Name: {model_name}
|
| 96 |
+
- Category: {category}
|
| 97 |
+
- Sub-category: {sub_category}
|
| 98 |
+
- Current Description: {current_description}
|
| 99 |
+
|
| 100 |
+
Write a professional, concise product description in less than 30 words.
|
| 101 |
+
Focus on key features, capabilities, and benefits. Make it suitable for a business quote.
|
| 102 |
+
|
| 103 |
+
Write only the description of the product, no additional text."""
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def generate_product_description(product_info: dict, llm) -> str:
|
| 107 |
+
"""
|
| 108 |
+
Generate a concise product description using LLM.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
product_info: Dictionary containing product details
|
| 112 |
+
llm: Language model instance
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
Generated product description (max 30 words)
|
| 116 |
+
"""
|
| 117 |
+
try:
|
| 118 |
+
prompt = DESCRIPTION_GENERATION_PROMPT.format(
|
| 119 |
+
manufacturer=product_info.get('manufacturer', 'N/A'),
|
| 120 |
+
model_name=product_info.get('model_name', 'N/A'),
|
| 121 |
+
category=product_info.get('category', 'N/A'),
|
| 122 |
+
sub_category=product_info.get('sub_category', 'N/A'),
|
| 123 |
+
current_description=product_info.get('description', 'N/A')
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
response = llm.invoke(prompt)
|
| 127 |
+
description = response.content.strip()
|
| 128 |
+
|
| 129 |
+
return description
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
# Fallback to a short version of the original description
|
| 133 |
+
original_desc = product_info.get('description', 'Professional technology equipment')
|
| 134 |
+
words = original_desc.split()
|
| 135 |
+
if len(words) > 30:
|
| 136 |
+
return ' '.join(words[:30])
|
| 137 |
+
return original_desc
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def format_currency(amount: float, currency: str) -> str:
|
| 141 |
+
"""Format currency amount with proper symbol."""
|
| 142 |
+
symbols = {"EUR": "€", "USD": "$", "HUF": "Ft"}
|
| 143 |
+
symbol = symbols.get(currency, currency)
|
| 144 |
+
|
| 145 |
+
if currency == "HUF":
|
| 146 |
+
return f"{amount:,.0f} {symbol}"
|
| 147 |
+
else:
|
| 148 |
+
return f"{symbol}{amount:,.2f}"
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def generate_markdown_quote(quote: Quote) -> str:
|
| 152 |
+
"""
|
| 153 |
+
Generate a professional quote in Markdown format.
|
| 154 |
+
"""
|
| 155 |
+
# Format dates
|
| 156 |
+
created_str = quote.created_date.strftime("%B %d, %Y")
|
| 157 |
+
valid_until_str = quote.valid_until.strftime("%B %d, %Y")
|
| 158 |
+
|
| 159 |
+
# Build customer section
|
| 160 |
+
customer_section = f"**{quote.customer.name}**"
|
| 161 |
+
if quote.customer.company:
|
| 162 |
+
customer_section += f" \n{quote.customer.company}"
|
| 163 |
+
if quote.customer.email:
|
| 164 |
+
customer_section += f" \n📧 {quote.customer.email}"
|
| 165 |
+
|
| 166 |
+
# Create Markdown table for products
|
| 167 |
+
markdown_table = """
|
| 168 |
+
| Product Name | Product ID | Description | Quantity | Unit Price | Total Price |
|
| 169 |
+
|--------------|------------|-------------|----------|------------|-------------|
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
for item in quote.items:
|
| 173 |
+
# Clean description for table (remove line breaks but keep full text)
|
| 174 |
+
clean_desc = item.description.replace('\n', ' ').replace('\r', ' ')
|
| 175 |
+
|
| 176 |
+
unit_price_str = format_currency(item.unit_price, quote.currency)
|
| 177 |
+
total_price_str = format_currency(item.total_price, quote.currency)
|
| 178 |
+
|
| 179 |
+
markdown_table += f"| {item.product_name} | {item.display_product_id} | {clean_desc} | {item.quantity} | {unit_price_str} | {total_price_str} |\n"
|
| 180 |
+
|
| 181 |
+
# Grand total
|
| 182 |
+
grand_total_str = format_currency(quote.grand_total, quote.currency)
|
| 183 |
+
|
| 184 |
+
# Complete Markdown template
|
| 185 |
+
markdown_template = f"""# Professional Product Quotation
|
| 186 |
+
|
| 187 |
+
## STREAMNET SOLUTIONS
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
**Quote ID:** {quote.quote_id}
|
| 192 |
+
**Date:** {created_str}
|
| 193 |
+
**Valid Until:** {valid_until_str}
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## Quote To:
|
| 198 |
+
|
| 199 |
+
{customer_section}
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
{{greeting}}
|
| 204 |
+
|
| 205 |
+
{{introduction}}
|
| 206 |
+
|
| 207 |
+
---
|
| 208 |
+
|
| 209 |
+
## Products
|
| 210 |
+
|
| 211 |
+
{markdown_table}
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## **Grand Total: {grand_total_str}**
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
## Terms and Conditions
|
| 220 |
+
|
| 221 |
+
- This quotation is valid for 30 days from the date of issue
|
| 222 |
+
- Prices are subject to change without notice after expiration
|
| 223 |
+
- Payment terms: Net 30 days from invoice date
|
| 224 |
+
- All prices exclude shipping and handling unless otherwise specified
|
| 225 |
+
- Products are subject to availability
|
| 226 |
+
- Technical specifications may vary, please confirm before ordering
|
| 227 |
+
- Returns accepted within 14 days in original condition
|
| 228 |
+
- Warranty terms as per manufacturer specifications
|
| 229 |
+
|
| 230 |
+
---
|
| 231 |
+
|
| 232 |
+
### Contact Information
|
| 233 |
+
|
| 234 |
+
📧 **Email:** sales@streamnet.com
|
| 235 |
+
📞 **Phone:** +1 (555) 123-4567
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
*Generated on {created_str}*
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
return markdown_template
|
src/sales_assistant/created_quotes/quote_QT-B4849588_Kristof_Proba_20250825_114359.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Professional Product Quotation
|
| 2 |
+
|
| 3 |
+
## STREAMNET SOLUTIONS
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
**Quote ID:** QT-B4849588
|
| 8 |
+
**Date:** August 25, 2025
|
| 9 |
+
**Valid Until:** September 24, 2025
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Quote To:
|
| 14 |
+
|
| 15 |
+
**Kristof Proba**
|
| 16 |
+
📧 kristofkristof@gmail.com
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
Option 1: Dear Kristof Proba, thank you for your interest—please find your tailored quotation below.
|
| 21 |
+
|
| 22 |
+
Option 2: Hello Kristof, we’re pleased to provide the following quote and look forward to assisting you.
|
| 23 |
+
|
| 24 |
+
Kristof Proba, please find a quote for two items: the Saber U20 (WHITE) and the Saber 5X (Saber Light). We’re confident these products will meet your needs and deliver the reliable performance you expect.
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## Products
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
| Product Name | Product ID | Description | Quantity | Unit Price | Total Price |
|
| 32 |
+
|--------------|------------|-------------|----------|------------|-------------|
|
| 33 |
+
| Saber U20 (WHITE) | ANG2-20FHD-01W | Angekis Saber U20 (White) — compact professional camera delivering high-quality imaging, reliable performance, versatile mounting and connectivity, and a discreet white finish for seamless commercial integration. | 2 | 312,460 Ft | 624,920 Ft |
|
| 34 |
+
| Saber 5X (Saber Light) | U3-5FHD6 | angekis Saber 5X (Saber Light) PTZ camera — 5× zoom, 90° HFOV, simultaneous USB 3.0 60 fps and RS232 connectivity for high-frame-rate capture and seamless integration. | 3 | 321,300 Ft | 963,900 Ft |
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## **Grand Total: 1,588,820 Ft**
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## Terms and Conditions
|
| 44 |
+
|
| 45 |
+
- This quotation is valid for 30 days from the date of issue
|
| 46 |
+
- Prices are subject to change without notice after expiration
|
| 47 |
+
- Payment terms: Net 30 days from invoice date
|
| 48 |
+
- All prices exclude shipping and handling unless otherwise specified
|
| 49 |
+
- Products are subject to availability
|
| 50 |
+
- Technical specifications may vary, please confirm before ordering
|
| 51 |
+
- Returns accepted within 14 days in original condition
|
| 52 |
+
- Warranty terms as per manufacturer specifications
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
### Contact Information
|
| 57 |
+
|
| 58 |
+
📧 **Email:** sales@streamnet.com
|
| 59 |
+
📞 **Phone:** +1 (555) 123-4567
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
*Generated on August 25, 2025*
|
src/sales_assistant/db/utils/db_connections.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database utilities for MySQL operations through SSH tunnel.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
load_dotenv()
|
| 8 |
+
import pandas as pd
|
| 9 |
+
from sqlalchemy import create_engine, text
|
| 10 |
+
from sshtunnel import SSHTunnelForwarder
|
| 11 |
+
import pymysql
|
| 12 |
+
from typing import Optional, Union, List, Dict, Any
|
| 13 |
+
|
| 14 |
+
# SSH Tunnel details
|
| 15 |
+
ssh_hostname = os.getenv('SSH_HOSTNAME')
|
| 16 |
+
ssh_port = int(os.getenv('SSH_PORT', 22))
|
| 17 |
+
ssh_username = os.getenv('SSH_USERNAME')
|
| 18 |
+
ssh_pkey_path = os.getenv('SSH_PKEY_PATH')
|
| 19 |
+
|
| 20 |
+
# Convert relative path to absolute path if needed
|
| 21 |
+
if ssh_pkey_path and not os.path.isabs(ssh_pkey_path):
|
| 22 |
+
# Get the current file's directory and navigate to find the correct path
|
| 23 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 24 |
+
print(f"Current directory: {current_dir}")
|
| 25 |
+
# We're in db/utils/, and the path in .env is "db/pemfile/db_pem.pem"
|
| 26 |
+
# So we need to go up to sales_assistant/ and then follow the path
|
| 27 |
+
sales_assistant_dir = os.path.dirname(os.path.dirname(current_dir))
|
| 28 |
+
ssh_pkey_path = os.path.join(sales_assistant_dir, ssh_pkey_path)
|
| 29 |
+
print(f"Converted SSH_PKEY_PATH to absolute path: {ssh_pkey_path}")
|
| 30 |
+
|
| 31 |
+
# MySQL database details
|
| 32 |
+
mysql_host = os.getenv('MYSQL_HOST')
|
| 33 |
+
mysql_port = int(os.getenv('MYSQL_PORT', 3306))
|
| 34 |
+
mysql_user = os.getenv('MYSQL_USER')
|
| 35 |
+
mysql_password = os.getenv('MYSQL_PASSWORD')
|
| 36 |
+
mysql_db = os.getenv('MYSQL_DB')
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def validate_config(required_vars):
|
| 40 |
+
|
| 41 |
+
missing_vars = [var for var in required_vars if not os.getenv(var)]
|
| 42 |
+
if len(missing_vars) == 0:
|
| 43 |
+
is_valid = True
|
| 44 |
+
else:
|
| 45 |
+
is_valid = False
|
| 46 |
+
|
| 47 |
+
return_dict = {
|
| 48 |
+
"is_valid": is_valid,
|
| 49 |
+
"missing_vars": missing_vars
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return return_dict
|
| 53 |
+
|
| 54 |
+
def connect_ssh_tunnel():
|
| 55 |
+
"""
|
| 56 |
+
Establish an SSH tunnel to the MySQL server.
|
| 57 |
+
Returns the SSHTunnelForwarder object.
|
| 58 |
+
"""
|
| 59 |
+
try:
|
| 60 |
+
tunnel = SSHTunnelForwarder(
|
| 61 |
+
(ssh_hostname, ssh_port),
|
| 62 |
+
ssh_username=ssh_username,
|
| 63 |
+
ssh_pkey=ssh_pkey_path,
|
| 64 |
+
remote_bind_address=(mysql_host, mysql_port)
|
| 65 |
+
)
|
| 66 |
+
tunnel.start()
|
| 67 |
+
return tunnel
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Error establishing SSH tunnel: {e}")
|
| 70 |
+
raise
|
| 71 |
+
|
| 72 |
+
def disconnect(engine, tunnel):
|
| 73 |
+
"""
|
| 74 |
+
Disconnect the SQLAlchemy engine and stop the SSH tunnel.
|
| 75 |
+
"""
|
| 76 |
+
try:
|
| 77 |
+
if engine:
|
| 78 |
+
engine.dispose()
|
| 79 |
+
if tunnel:
|
| 80 |
+
tunnel.stop()
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"Error during disconnection: {e}")
|
| 83 |
+
raise
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def create_db_engine(tunnel: SSHTunnelForwarder, charset: str = "utf8mb4"):
|
| 87 |
+
"""
|
| 88 |
+
Create a SQLAlchemy engine connected to the MySQL database through the SSH tunnel.
|
| 89 |
+
"""
|
| 90 |
+
try:
|
| 91 |
+
local_port = tunnel.local_bind_port
|
| 92 |
+
database_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@127.0.0.1:{local_port}/{mysql_db}?charset={charset}"
|
| 93 |
+
engine = create_engine(
|
| 94 |
+
database_url,
|
| 95 |
+
connect_args={
|
| 96 |
+
"connect_timeout": 60*5,
|
| 97 |
+
"read_timeout": 60*5,
|
| 98 |
+
"write_timeout": 60*5,
|
| 99 |
+
"charset": charset
|
| 100 |
+
}
|
| 101 |
+
)
|
| 102 |
+
return engine
|
| 103 |
+
except Exception as e:
|
| 104 |
+
print(f"Error creating database engine: {e}")
|
| 105 |
+
raise
|
src/sales_assistant/db/utils/db_utils.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database utilities for querying and writing to MySQL through an SSH tunnel.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
load_dotenv()
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from sqlalchemy import text
|
| 9 |
+
from .db_connections import connect_ssh_tunnel, create_db_engine, disconnect
|
| 10 |
+
|
| 11 |
+
def read_sql(query_str: str) -> pd.DataFrame:
|
| 12 |
+
"""
|
| 13 |
+
Execute a read SQL query and return the results as a pandas DataFrame.
|
| 14 |
+
Uses pandas built-in read_sql_query method.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
query_str: SQL query string to execute
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
pandas DataFrame with query results
|
| 21 |
+
"""
|
| 22 |
+
tunnel = None
|
| 23 |
+
engine = None
|
| 24 |
+
try:
|
| 25 |
+
tunnel = connect_ssh_tunnel()
|
| 26 |
+
engine = create_db_engine(tunnel)
|
| 27 |
+
|
| 28 |
+
df = pd.read_sql_query(sql=text(query_str), con=engine)
|
| 29 |
+
|
| 30 |
+
return df
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"Error executing read query: {e}")
|
| 33 |
+
# return an empty DataFrame on error
|
| 34 |
+
return pd.DataFrame()
|
| 35 |
+
finally:
|
| 36 |
+
disconnect(engine, tunnel)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def write_to_table(df: pd.DataFrame, table_name: str, if_exists: str = 'append', schema: str = None) -> bool:
|
| 40 |
+
"""
|
| 41 |
+
Write a pandas DataFrame to a MySQL table using pandas built-in to_sql method.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
df: pandas DataFrame to write
|
| 45 |
+
table_name: Name of the target table
|
| 46 |
+
if_exists: What to do if table exists ('append', 'replace', 'fail')
|
| 47 |
+
schema: Database schema name (optional)
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
True if successful, False otherwise
|
| 51 |
+
"""
|
| 52 |
+
tunnel = None
|
| 53 |
+
engine = None
|
| 54 |
+
try:
|
| 55 |
+
tunnel = connect_ssh_tunnel()
|
| 56 |
+
engine = create_db_engine(tunnel)
|
| 57 |
+
|
| 58 |
+
df.to_sql(
|
| 59 |
+
name=table_name,
|
| 60 |
+
con=engine,
|
| 61 |
+
if_exists=if_exists,
|
| 62 |
+
index=False,
|
| 63 |
+
schema=schema,
|
| 64 |
+
method='multi',
|
| 65 |
+
chunksize=1000
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
print(f"Successfully wrote {len(df)} rows to table '{table_name}' (mode: {if_exists})")
|
| 69 |
+
return True
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"Error writing DataFrame to table: {e}")
|
| 73 |
+
return False
|
| 74 |
+
finally:
|
| 75 |
+
disconnect(engine, tunnel)
|
src/sales_assistant/main.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main entry point for the Sales Assistant application.
|
| 3 |
+
|
| 4 |
+
This file serves as the primary interface to run the sales assistant agent.
|
| 5 |
+
It uses the agent runner from agent_main/agent_runner.py to create and manage
|
| 6 |
+
the conversational agent with database exploration capabilities.
|
| 7 |
+
"""
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from typing import Optional
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
# Add the src directory to the path so we can import from sales_assistant
|
| 14 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 15 |
+
|
| 16 |
+
from sales_assistant.agent_main.agent_runner import (
|
| 17 |
+
ConversationConfig,
|
| 18 |
+
run_interactive_loop,
|
| 19 |
+
demo_agent_runner,
|
| 20 |
+
create_agent_runner,
|
| 21 |
+
run_conversation_turn
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Load environment variables
|
| 25 |
+
load_dotenv()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def check_environment_setup() -> bool:
|
| 29 |
+
"""
|
| 30 |
+
Check if the environment is properly set up for the agent.
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
True if environment is properly configured, False otherwise
|
| 34 |
+
"""
|
| 35 |
+
required_vars = [
|
| 36 |
+
"OPENAI_API_KEY",
|
| 37 |
+
"MODEL_PROVIDER",
|
| 38 |
+
"MODEL_NAME",
|
| 39 |
+
"MYSQL_HOST",
|
| 40 |
+
"MYSQL_USER",
|
| 41 |
+
"MYSQL_PASSWORD",
|
| 42 |
+
"MYSQL_DB"
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
missing_vars = []
|
| 46 |
+
for var in required_vars:
|
| 47 |
+
if not os.getenv(var):
|
| 48 |
+
missing_vars.append(var)
|
| 49 |
+
|
| 50 |
+
if missing_vars:
|
| 51 |
+
print("❌ Missing required environment variables:")
|
| 52 |
+
for var in missing_vars:
|
| 53 |
+
print(f" - {var}")
|
| 54 |
+
print("\n💡 Please set these variables in your .env file")
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
print("✅ Environment configuration looks good!")
|
| 58 |
+
return True
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def create_custom_config(
|
| 62 |
+
session_id: Optional[str] = None,
|
| 63 |
+
user_id: Optional[str] = None,
|
| 64 |
+
enable_langsmith: bool = True
|
| 65 |
+
) -> ConversationConfig:
|
| 66 |
+
"""
|
| 67 |
+
Create a custom configuration for the agent.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
session_id: Optional custom session ID
|
| 71 |
+
user_id: Optional user identifier
|
| 72 |
+
enable_langsmith: Whether to enable LangSmith tracing
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
ConversationConfig object
|
| 76 |
+
"""
|
| 77 |
+
config = ConversationConfig(
|
| 78 |
+
enable_langsmith=enable_langsmith,
|
| 79 |
+
langsmith_project=os.getenv("LANGSMITH_PROJECT", "sales-assistant-prod")
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
if session_id:
|
| 83 |
+
config.session_id = session_id
|
| 84 |
+
if user_id:
|
| 85 |
+
config.user_id = user_id
|
| 86 |
+
|
| 87 |
+
return config
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def run_sales_assistant(
|
| 91 |
+
interactive: bool = True,
|
| 92 |
+
demo_mode: bool = False,
|
| 93 |
+
custom_config: Optional[ConversationConfig] = None
|
| 94 |
+
) -> None:
|
| 95 |
+
"""
|
| 96 |
+
Main function to run the sales assistant.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
interactive: Whether to run in interactive mode
|
| 100 |
+
demo_mode: Whether to run demo conversations
|
| 101 |
+
custom_config: Optional custom configuration
|
| 102 |
+
"""
|
| 103 |
+
print("🚀 Starting Sales Assistant...")
|
| 104 |
+
|
| 105 |
+
# Check environment setup
|
| 106 |
+
if not check_environment_setup():
|
| 107 |
+
return
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
if demo_mode:
|
| 111 |
+
print("🎮 Running in demo mode...")
|
| 112 |
+
demo_agent_runner()
|
| 113 |
+
elif interactive:
|
| 114 |
+
print("💬 Starting interactive mode...")
|
| 115 |
+
config = custom_config or ConversationConfig()
|
| 116 |
+
run_interactive_loop(config)
|
| 117 |
+
else:
|
| 118 |
+
print("⚙️ Agent initialized and ready for programmatic use")
|
| 119 |
+
config = custom_config or ConversationConfig()
|
| 120 |
+
compiled_graph, checkpointer, langsmith_client, thread_id = create_agent_runner(config)
|
| 121 |
+
print(f"📊 Session ID: {config.session_id}")
|
| 122 |
+
print(f"🧵 Thread ID: {thread_id}")
|
| 123 |
+
print("💡 Use the returned objects to run conversations programmatically")
|
| 124 |
+
return compiled_graph, checkpointer, langsmith_client, thread_id
|
| 125 |
+
|
| 126 |
+
except KeyboardInterrupt:
|
| 127 |
+
print("\n👋 Sales Assistant stopped by user")
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f"❌ Error running Sales Assistant: {str(e)}")
|
| 130 |
+
sys.exit(1)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def single_query(query: str, config: Optional[ConversationConfig] = None) -> str:
|
| 134 |
+
"""
|
| 135 |
+
Run a single query against the agent without interactive mode.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
query: The question to ask the agent
|
| 139 |
+
config: Optional configuration
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
Agent's response
|
| 143 |
+
"""
|
| 144 |
+
if not check_environment_setup():
|
| 145 |
+
return "Environment not properly configured"
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
config = config or ConversationConfig()
|
| 149 |
+
compiled_graph, checkpointer, langsmith_client, thread_id = create_agent_runner(config)
|
| 150 |
+
|
| 151 |
+
response = run_conversation_turn(
|
| 152 |
+
compiled_graph,
|
| 153 |
+
thread_id,
|
| 154 |
+
query,
|
| 155 |
+
langsmith_client
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
return response
|
| 159 |
+
|
| 160 |
+
except Exception as e:
|
| 161 |
+
return f"Error processing query: {str(e)}"
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def main():
|
| 165 |
+
"""
|
| 166 |
+
Main entry point when running the script directly.
|
| 167 |
+
"""
|
| 168 |
+
import argparse
|
| 169 |
+
|
| 170 |
+
parser = argparse.ArgumentParser(
|
| 171 |
+
description="Sales Assistant - Database-powered product inquiry agent"
|
| 172 |
+
)
|
| 173 |
+
parser.add_argument(
|
| 174 |
+
"--mode",
|
| 175 |
+
choices=["interactive", "demo", "single"],
|
| 176 |
+
default="interactive",
|
| 177 |
+
help="Mode to run the assistant in"
|
| 178 |
+
)
|
| 179 |
+
parser.add_argument(
|
| 180 |
+
"--query",
|
| 181 |
+
type=str,
|
| 182 |
+
help="Single query to run (only for single mode)"
|
| 183 |
+
)
|
| 184 |
+
parser.add_argument(
|
| 185 |
+
"--no-langsmith",
|
| 186 |
+
action="store_true",
|
| 187 |
+
help="Disable LangSmith tracing"
|
| 188 |
+
)
|
| 189 |
+
parser.add_argument(
|
| 190 |
+
"--session-id",
|
| 191 |
+
type=str,
|
| 192 |
+
help="Custom session ID"
|
| 193 |
+
)
|
| 194 |
+
parser.add_argument(
|
| 195 |
+
"--user-id",
|
| 196 |
+
type=str,
|
| 197 |
+
help="User identifier"
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
args = parser.parse_args()
|
| 201 |
+
|
| 202 |
+
# Create configuration
|
| 203 |
+
config = create_custom_config(
|
| 204 |
+
session_id=args.session_id,
|
| 205 |
+
user_id=args.user_id,
|
| 206 |
+
enable_langsmith=not args.no_langsmith
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if args.mode == "single":
|
| 210 |
+
if not args.query:
|
| 211 |
+
print("❌ --query is required for single mode")
|
| 212 |
+
sys.exit(1)
|
| 213 |
+
|
| 214 |
+
print(f"🔍 Processing query: {args.query}")
|
| 215 |
+
response = single_query(args.query, config)
|
| 216 |
+
print(f"🤖 Response: {response}")
|
| 217 |
+
|
| 218 |
+
elif args.mode == "demo":
|
| 219 |
+
run_sales_assistant(interactive=False, demo_mode=True, custom_config=config)
|
| 220 |
+
|
| 221 |
+
else: # interactive mode
|
| 222 |
+
run_sales_assistant(interactive=True, demo_mode=False, custom_config=config)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
if __name__ == "__main__":
|
| 226 |
+
main()
|
src/sales_assistant/prompts/__init__.py
ADDED
|
File without changes
|
src/sales_assistant/prompts/system_prompt.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
SYSTEM_PROMPT = """You are an intelligent database exploration and sales assistant agent. You help users explore a MySQL database and answer questions about products using dynamic SQL queries.
|
| 3 |
+
|
| 4 |
+
## Your Mission
|
| 5 |
+
1. **Explore First**: When a user asks a question, start by exploring the database structure and content using custom SQL queries. This should be broad.
|
| 6 |
+
- tips: start wth the manifcaturer, get the categories and subcategories and then look into the descipton column to find products
|
| 7 |
+
2. **Understand the Data**: Use multiple tools with creative SQL queries to understand what products are available
|
| 8 |
+
3. **Query Strategically**: Build sophisticated SQL queries based on your exploration findings run multiple queries to get a complete picture
|
| 9 |
+
4. **Answer Comprehensively**: Provide helpful, detailed answers with specific product information
|
| 10 |
+
|
| 11 |
+
## Database Information
|
| 12 |
+
- Main table: `streamnet.products_list`
|
| 13 |
+
- Key columns: id, manufacturer, category, sub_category, model_name, model_number_long, model_number_short, description, currency, distributor_pricing, msrp
|
| 14 |
+
- Contains various products with pricing and detailed information from various manufacturers
|
| 15 |
+
- different manufacturers can have different definitions how they categorize their products, so you need to be creative in how you search for products with custom SQL queries
|
| 16 |
+
|
| 17 |
+
## Information about user's intent:
|
| 18 |
+
- if a user asks about specific product you need to use the model_name, or model_number_long, or model_number_short to find the product
|
| 19 |
+
- if a user asks about a non specific product you can use the manifcaturer, category and sub_category fields to find the products more generally e.g.
|
| 20 |
+
- the description column can contain additional information about the product like: size, resolution, etc so you can creatively use it to find products
|
| 21 |
+
|
| 22 |
+
## Available Dynamic SQL Tools
|
| 23 |
+
All tools now accept custom SQL queries as input, giving you complete flexibility:
|
| 24 |
+
|
| 25 |
+
- `describe_table`: Use custom SQL to explore table structure (DESCRIBE, SHOW COLUMNS, information_schema queries)
|
| 26 |
+
- `get_sample_data`: Use dynamic SQL with WHERE clauses, JOINs, filtering to sample specific data subsets
|
| 27 |
+
- `get_distinct_values`: Use GROUP BY, aggregations, complex filtering to explore data patterns and distributions
|
| 28 |
+
- `search_products_by_criteria`: Use sophisticated WHERE clauses, LIKE patterns, ranges, sorting for product searches
|
| 29 |
+
- `get_table_statistics`: Use aggregation functions (COUNT, AVG, MIN, MAX, STDDEV) for analytical insights
|
| 30 |
+
- `execute_advanced_query`: Run any complex SQL including subqueries, CTEs, window functions, advanced analytics
|
| 31 |
+
|
| 32 |
+
## Avalable Non-SQL Tools
|
| 33 |
+
- `exchange_converter`: Convert between EUR, USD, and HUF currencies
|
| 34 |
+
- `create_quote`: Generate professional quotes with multiple products, customer info, and LLM-generated content
|
| 35 |
+
|
| 36 |
+
## Your Enhanced ReAct Process with Dynamic SQL
|
| 37 |
+
1. **Reason**: Think about what information you need and what SQL query would best retrieve it, but always start with exploring the database structure and if you need by manufacturer, category and sub_category the structure one by one goind deeper
|
| 38 |
+
2. **Act**: Craft custom SQL queries using the appropriate tools for maximum flexibility trying out different queries to explore the database
|
| 39 |
+
3. **Observe**: Analyze the results from your SQL queries
|
| 40 |
+
4. **Iterate**: Refine your SQL queries based on results, add filters, change aggregations, explore different angles
|
| 41 |
+
5. **Respond**: Provide a comprehensive answer with specific product details
|
| 42 |
+
|
| 43 |
+
## Advanced SQL examples and tips
|
| 44 |
+
- Use WHERE clauses creatively to filter data: `WHERE manufacturer = 'Samsung' AND category = 'Electronics AND sub_category'`
|
| 45 |
+
- Use LIKE for pattern matching: `WHERE description LIKE '%50%' OR model_name LIKE '%Pro%'`
|
| 46 |
+
- Use aggregations for insights: `SELECT manufacturer, COUNT(*), AVG(msrp) FROM ... GROUP BY manufacturer`
|
| 47 |
+
- Use ORDER BY for meaningful sorting: `ORDER BY msrp DESC, manufacturer ASC`
|
| 48 |
+
- Use LIMIT to control result size: `LIMIT 10` or `LIMIT 20` you an go even higher to get more results
|
| 49 |
+
- Combine multiple conditions: `WHERE msrp BETWEEN 100 AND 500 AND manufacturer IN ('Apple', 'Samsung')`
|
| 50 |
+
- Use subqueries for complex logic: `WHERE msrp > (SELECT AVG(msrp) FROM streamnet.products_list)`
|
| 51 |
+
|
| 52 |
+
## SQL Query Examples for Inspiration
|
| 53 |
+
- `SELECT * FROM streamnet.products_list WHERE manufacturer = 'Samsung' AND model_name like '%Entry%'`
|
| 54 |
+
- `SELECT DISTINCT category FROM streamnet.products_list WHERE manufacturer like 'angekis'`
|
| 55 |
+
- `SELECT * FROM streamnet.products_list WHERE category = 'Camera'
|
| 56 |
+
|
| 57 |
+
## Price list description
|
| 58 |
+
- For sony products the category and sub_category is not desciptive e.g. Professional BRAVIA Full HD & 4K are tvs, and Video Wall are led displays
|
| 59 |
+
- For sony you need to look at the description column to find the products and their categories too
|
| 60 |
+
|
| 61 |
+
## Guidelines
|
| 62 |
+
- If the user intent is straightforward, use the most relevant tool with a custom SQL query
|
| 63 |
+
- If not always start with database exploration using custom SQL queries in this way you can find the products more easily
|
| 64 |
+
- Be creative with your SQL - use complex WHERE clauses, JOINs, subqueries as needed
|
| 65 |
+
- Use multiple tools with different SQL queries to get a complete picture if needed
|
| 66 |
+
- Provide specific product details including pricing when a user ask for it
|
| 67 |
+
- the `exchange_converter` tool can be used to convert prices if the user asks for it or if you want to normalize prices to a common currency
|
| 68 |
+
- **Quote Generation**: When users want quotes, use the `create_quote` tool with:
|
| 69 |
+
- Customer information (name, email, company)
|
| 70 |
+
- Product id-s of the products they want quotes for (use multiple if needed)
|
| 71 |
+
- Appropriate currency
|
| 72 |
+
- The tool generates professional quotes with LLM-enhanced greetings and introductions
|
| 73 |
+
|
| 74 |
+
## Other Guidelines
|
| 75 |
+
- Ask follow-up questions if the user's request is unclear
|
| 76 |
+
- Build upon previous SQL query results to refine your search
|
| 77 |
+
- Remember context from previous explorations in the conversation
|
| 78 |
+
|
| 79 |
+
If you want you can start by exploring the database structure with a custom SQL query to understand what you're working with!"""
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
SYSTEM_PROMPT = """You are an intelligent database exploration and sales assistant agent. You help users explore a MySQL database and answer questions about products using dynamic SQL queries.
|
| 83 |
+
|
| 84 |
+
## Your Mission
|
| 85 |
+
1. **Explore First**: When a user asks a question, start by exploring the database structure and content using custom SQL queries. This should be broad.
|
| 86 |
+
- tips: start wth the manufcaturer, get the categories and subcategories, get some samples and then look into the descipton column to find products
|
| 87 |
+
2. **Understand the Data**: Use multiple tools with creative SQL queries to understand what products are available if needed
|
| 88 |
+
3. **Query Strategically**: Build sophisticated SQL queries based on your exploration findings run multiple queries to get a complete picture
|
| 89 |
+
4. **Answer Comprehensively**: Provide helpful, detailed answers with specific product information
|
| 90 |
+
|
| 91 |
+
## Database Information
|
| 92 |
+
- Main table: `streamnet.products_list`
|
| 93 |
+
- Key columns: id, manufacturer, category, sub_category, model_name, model_number_long, model_number_short, description, currency, distributor_pricing, msrp
|
| 94 |
+
- Contains various products with pricing and detailed information from various manufacturers
|
| 95 |
+
- different manufacturers can have different definitions how they categorize their products, so you need to be creative in how you search for products with custom SQL queries
|
| 96 |
+
|
| 97 |
+
## Manufacturer specific tips
|
| 98 |
+
- For sony products the category and sub_category is not desciptive e.g. Professional BRAVIA Full HD & 4K are tvs, and Video Wall are led displays
|
| 99 |
+
- For sony you need to look at the description column to find the products and their categories too
|
| 100 |
+
|
| 101 |
+
## Information about user's intent:
|
| 102 |
+
- if a user asks about specific product you need to use the model_name, or model_number_long, or model_number_short to find the product
|
| 103 |
+
- if a user asks about a non specific product you can use the manifcaturer, category and sub_category fields to find the products more generally e.g.
|
| 104 |
+
- the description column can contain additional information about the product like: size, resolution, etc so you can creatively use it to find products
|
| 105 |
+
|
| 106 |
+
## Available Dynamic SQL Tools
|
| 107 |
+
- `execute_sql_query`: Use custom SQL to explore table structure, and query data with complete flexibility
|
| 108 |
+
|
| 109 |
+
## Avalable Non-SQL Tools
|
| 110 |
+
- `exchange_converter`: Convert between EUR, USD, and HUF currencies
|
| 111 |
+
- `create_quote`: Generate professional quotes with multiple products, customer info, and LLM-generated content
|
| 112 |
+
|
| 113 |
+
## Your Enhanced ReAct Process with Dynamic SQL
|
| 114 |
+
1. **Reason**: Think about what information you need and what SQL query would best retrieve it, but always start with exploring the database structure and if you need by manufacturer, category and sub_category the structure one by one goind deeper
|
| 115 |
+
2. **Act**: Craft custom SQL queries using the appropriate tools for maximum flexibility trying out different queries to explore the database
|
| 116 |
+
3. **Observe**: Analyze the results from your SQL queries
|
| 117 |
+
4. **Iterate**: Refine your SQL queries based on results, add filters, change aggregations, explore different angles
|
| 118 |
+
5. **Respond**: Provide a comprehensive answer with specific product details
|
| 119 |
+
|
| 120 |
+
## Guidelines
|
| 121 |
+
- If the user intent is straightforward, use the most relevant tool with a custom SQL query
|
| 122 |
+
- If not always start with database exploration using custom SQL queries in this way you can find the products more easily
|
| 123 |
+
- Be creative with your SQL - use complex WHERE clauses, JOINs, subqueries as needed
|
| 124 |
+
- Use limit to focus your results and minimize token usage as much as possible
|
| 125 |
+
- Use multiple tools with different SQL queries to get a complete picture if needed
|
| 126 |
+
- Provide specific product details including pricing when a user ask for it
|
| 127 |
+
- the `exchange_converter` tool can be used to convert prices if the user asks for it or if you want to normalize prices to a common currency
|
| 128 |
+
- **Quote Generation**: When users want quotes, use the `create_quote` tool with:
|
| 129 |
+
- Customer information (name, email)
|
| 130 |
+
- Product id-s of the products they want quotes for (use multiple if needed)
|
| 131 |
+
- Appropriate currency
|
| 132 |
+
- The tool generates professional quotes with LLM-enhanced greetings and introductions
|
| 133 |
+
|
| 134 |
+
## Other Guidelines
|
| 135 |
+
- Ask follow-up questions if the user's request is unclear
|
| 136 |
+
- Avoid offering options that you cannot fulfill like (generating pdf, send emails, etc)
|
| 137 |
+
- If the user want to compare products pull them up with a single SQL query using IN
|
| 138 |
+
- Build upon previous SQL query results to refine your search
|
| 139 |
+
- Remember context from previous explorations in the conversation
|
| 140 |
+
|
| 141 |
+
If you want you can start by exploring the database structure with a custom SQL query to understand what you're working with!"""
|
test_files/db_test.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sales_assistant.db.utils.db_connections import validate_config
|
| 2 |
+
from sales_assistant.db.utils.db_utils import read_sql, write_to_table
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
# Validate configuration
|
| 6 |
+
required_vars = [
|
| 7 |
+
'SSH_HOSTNAME', 'SSH_USERNAME', 'SSH_PKEY_PATH',
|
| 8 |
+
'MYSQL_HOST', 'MYSQL_USER', 'MYSQL_PASSWORD', 'MYSQL_DB'
|
| 9 |
+
]
|
| 10 |
+
validate_config_vars = validate_config(required_vars)
|
| 11 |
+
print("Config validation:", validate_config_vars)
|
| 12 |
+
|
| 13 |
+
# Test basic connection
|
| 14 |
+
print("\n=== Testing basic connection ===")
|
| 15 |
+
test_connection = read_sql("SELECT 1 as test_column;")
|
| 16 |
+
print("Connection test result:")
|
| 17 |
+
print(test_connection)
|
| 18 |
+
|
| 19 |
+
# Test reading from products table
|
| 20 |
+
print("\n=== Testing products table read ===")
|
| 21 |
+
test_product_table = read_sql("SELECT * FROM streamnet.products_list LIMIT 5;")
|
| 22 |
+
print("Products table sample:")
|
| 23 |
+
print(test_product_table.head())
|
| 24 |
+
print(f"Shape: {test_product_table.shape}")
|
| 25 |
+
|
| 26 |
+
# Test writing a sample DataFrame
|
| 27 |
+
print("\n=== Testing DataFrame write (create new table) ===")
|
| 28 |
+
sample_data = pd.DataFrame({
|
| 29 |
+
'id': [1, 2, 3],
|
| 30 |
+
'name': ['Test Product 1', 'Test Product 2', 'Test Product 3'],
|
| 31 |
+
'price': [10.99, 20.50, 15.75],
|
| 32 |
+
'description': ['Sample description 1', 'Sample description 2', 'Sample description 3']
|
| 33 |
+
})
|
| 34 |
+
|
| 35 |
+
print("Sample data to write:")
|
| 36 |
+
print(sample_data)
|
| 37 |
+
|
| 38 |
+
# Create new table using convenience function
|
| 39 |
+
success = write_to_table(
|
| 40 |
+
df=sample_data,
|
| 41 |
+
table_name='test_products',
|
| 42 |
+
schema='streamnet',
|
| 43 |
+
if_exists='replace' # Use replace to create new table
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
if success:
|
| 47 |
+
print("✅ Create table test successful!")
|
| 48 |
+
|
| 49 |
+
# Test appending more data
|
| 50 |
+
print("\n=== Testing DataFrame append ===")
|
| 51 |
+
additional_data = pd.DataFrame({
|
| 52 |
+
'id': [4, 5],
|
| 53 |
+
'name': ['Test Product 4', 'Test Product 5'],
|
| 54 |
+
'price': [25.00, 30.99],
|
| 55 |
+
'description': ['Sample description 4', 'Sample description 5']
|
| 56 |
+
})
|
| 57 |
+
|
| 58 |
+
print("Additional data to append:")
|
| 59 |
+
print(additional_data)
|
| 60 |
+
|
| 61 |
+
append_success = write_to_table(
|
| 62 |
+
df=additional_data,
|
| 63 |
+
table_name='test_products',
|
| 64 |
+
schema='streamnet',
|
| 65 |
+
if_exists='append'
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
if append_success:
|
| 69 |
+
print("✅ Append test successful!")
|
| 70 |
+
|
| 71 |
+
# Read back all the data
|
| 72 |
+
print("\n=== Verifying all written data ===")
|
| 73 |
+
read_back = read_sql("SELECT * FROM streamnet.test_products ORDER BY id;")
|
| 74 |
+
print("All data in test table:")
|
| 75 |
+
print(read_back)
|
| 76 |
+
print(f"Total rows: {len(read_back)}")
|
| 77 |
+
else:
|
| 78 |
+
print("❌ Append test failed!")
|
| 79 |
+
|
| 80 |
+
else:
|
| 81 |
+
print("❌ Create table test failed!")
|
test_files/quote_test.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script for the quote generation tool.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Add the src directory to the path so we can import from sales_assistant
|
| 9 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
| 10 |
+
|
| 11 |
+
from sales_assistant.agent_tools.create_quote import create_quote, create_sample_quote
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_basic_quote():
|
| 15 |
+
"""Test basic quote creation functionality."""
|
| 16 |
+
print("=== Testing Basic Quote Creation ===")
|
| 17 |
+
|
| 18 |
+
# Test data
|
| 19 |
+
sample_items = [
|
| 20 |
+
{
|
| 21 |
+
"product_name": "Samsung Galaxy S24 Ultra",
|
| 22 |
+
"description": "Samsung Galaxy S24 Ultra 512GB, Titanium Black, 5G smartphone with S Pen",
|
| 23 |
+
"quantity": 1,
|
| 24 |
+
"unit_price": 1399.99,
|
| 25 |
+
"currency": "EUR"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"product_name": "Apple MacBook Pro 14\"",
|
| 29 |
+
"description": "MacBook Pro 14-inch, M3 Pro chip, 18GB RAM, 512GB SSD, Space Black",
|
| 30 |
+
"quantity": 2,
|
| 31 |
+
"unit_price": 2499.00,
|
| 32 |
+
"currency": "EUR"
|
| 33 |
+
}
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
# Invoke the tool
|
| 37 |
+
result = create_quote.invoke({
|
| 38 |
+
"customer_name": "Alice Johnson",
|
| 39 |
+
"customer_email": "alice.johnson@company.com",
|
| 40 |
+
"customer_company": "Innovation Labs Inc.",
|
| 41 |
+
"customer_address": "456 Tech Street, Silicon Valley, CA 94000",
|
| 42 |
+
"customer_phone": "+1 (555) 987-6543",
|
| 43 |
+
"items": sample_items,
|
| 44 |
+
"target_currency": "EUR",
|
| 45 |
+
"tax_rate": 0.21, # 21% VAT
|
| 46 |
+
"notes": "Corporate discount applied"
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
print("Result:")
|
| 50 |
+
print(result)
|
| 51 |
+
return result
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_currency_conversion():
|
| 55 |
+
"""Test quote creation with currency conversion."""
|
| 56 |
+
print("\n=== Testing Currency Conversion ===")
|
| 57 |
+
|
| 58 |
+
# Items in different currencies
|
| 59 |
+
mixed_currency_items = [
|
| 60 |
+
{
|
| 61 |
+
"product_name": "Dell XPS 15",
|
| 62 |
+
"description": "Dell XPS 15 laptop, Intel i7, 16GB RAM, 1TB SSD",
|
| 63 |
+
"quantity": 1,
|
| 64 |
+
"unit_price": 1899.99,
|
| 65 |
+
"currency": "USD" # Will be converted to EUR
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"product_name": "Sony Camera",
|
| 69 |
+
"description": "Sony Alpha 7R V mirrorless camera with 61MP sensor",
|
| 70 |
+
"quantity": 1,
|
| 71 |
+
"unit_price": 3899.00,
|
| 72 |
+
"currency": "EUR" # Already in target currency
|
| 73 |
+
}
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
result = create_quote.invoke({
|
| 77 |
+
"customer_name": "Bob Wilson",
|
| 78 |
+
"customer_email": "bob@techstudio.com",
|
| 79 |
+
"customer_company": "TechStudio Photography",
|
| 80 |
+
"items": mixed_currency_items,
|
| 81 |
+
"target_currency": "EUR"
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
print("Result:")
|
| 85 |
+
print(result)
|
| 86 |
+
return result
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_single_item_quote():
|
| 90 |
+
"""Test quote with single item."""
|
| 91 |
+
print("\n=== Testing Single Item Quote ===")
|
| 92 |
+
|
| 93 |
+
single_item = [{
|
| 94 |
+
"product_name": "iPad Pro 12.9\"",
|
| 95 |
+
"description": "iPad Pro 12.9-inch (6th generation) with M2 chip, 128GB, WiFi",
|
| 96 |
+
"quantity": 3,
|
| 97 |
+
"unit_price": 1199.00,
|
| 98 |
+
"currency": "EUR"
|
| 99 |
+
}]
|
| 100 |
+
|
| 101 |
+
result = create_quote.invoke({
|
| 102 |
+
"customer_name": "Carol Davis",
|
| 103 |
+
"customer_email": "carol@school.edu",
|
| 104 |
+
"customer_company": "Mountain View School District",
|
| 105 |
+
"items": single_item,
|
| 106 |
+
"target_currency": "EUR",
|
| 107 |
+
"tax_rate": 0.0, # Tax-exempt organization
|
| 108 |
+
"notes": "Educational institution - tax exempt"
|
| 109 |
+
})
|
| 110 |
+
|
| 111 |
+
print("Result:")
|
| 112 |
+
print(result)
|
| 113 |
+
return result
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def test_error_handling():
|
| 117 |
+
"""Test error handling in quote creation."""
|
| 118 |
+
print("\n=== Testing Error Handling ===")
|
| 119 |
+
|
| 120 |
+
# Test missing customer name
|
| 121 |
+
print("Test 1: Missing customer name")
|
| 122 |
+
result1 = create_quote.invoke({
|
| 123 |
+
"customer_name": "",
|
| 124 |
+
"items": [{"product_name": "Test", "description": "Test", "quantity": 1, "unit_price": 100}]
|
| 125 |
+
})
|
| 126 |
+
print("Result:", result1)
|
| 127 |
+
|
| 128 |
+
# Test empty items
|
| 129 |
+
print("\nTest 2: Empty items list")
|
| 130 |
+
result2 = create_quote.invoke({
|
| 131 |
+
"customer_name": "Test Customer",
|
| 132 |
+
"items": []
|
| 133 |
+
})
|
| 134 |
+
print("Result:", result2)
|
| 135 |
+
|
| 136 |
+
# Test invalid item data
|
| 137 |
+
print("\nTest 3: Invalid item data")
|
| 138 |
+
result3 = create_quote.invoke({
|
| 139 |
+
"customer_name": "Test Customer",
|
| 140 |
+
"items": [{"product_name": "Test", "quantity": "invalid", "unit_price": "invalid"}]
|
| 141 |
+
})
|
| 142 |
+
print("Result:", result3)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_sample_quote_function():
|
| 146 |
+
"""Test the sample quote creation function."""
|
| 147 |
+
print("\n=== Testing Sample Quote Function ===")
|
| 148 |
+
|
| 149 |
+
result = create_sample_quote()
|
| 150 |
+
print("Sample quote result:")
|
| 151 |
+
print(result)
|
| 152 |
+
return result
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
print("🧪 Starting Quote Generation Tool Tests\n")
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
# Run all tests
|
| 160 |
+
test_basic_quote()
|
| 161 |
+
test_currency_conversion()
|
| 162 |
+
test_single_item_quote()
|
| 163 |
+
test_sample_quote_function()
|
| 164 |
+
test_error_handling()
|
| 165 |
+
|
| 166 |
+
print("\n✅ All tests completed!")
|
| 167 |
+
|
| 168 |
+
except Exception as e:
|
| 169 |
+
print(f"\n❌ Test failed with error: {e}")
|
| 170 |
+
import traceback
|
| 171 |
+
traceback.print_exc()
|
test_files/test_product_id_quotes.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script for the new product ID-based quote generation tool.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Add the src directory to the path
|
| 9 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
| 10 |
+
|
| 11 |
+
from sales_assistant.agent_tools.create_quote import create_quote, create_sample_quote_from_ids
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_product_id_quote():
|
| 15 |
+
"""Test the new product ID-based quote creation."""
|
| 16 |
+
print("🧪 Testing Product ID-based Quote Generation")
|
| 17 |
+
print("=" * 50)
|
| 18 |
+
|
| 19 |
+
# First, let's see what products are available in the database
|
| 20 |
+
print("📋 Testing with sample product IDs...")
|
| 21 |
+
|
| 22 |
+
# Test with some actual product IDs (including duplicates for quantity)
|
| 23 |
+
sample_product_ids = [760, 760, 763, 765, 765, 768] # 2x product 760, 1x product 763, 2x product 765, 1x product 768
|
| 24 |
+
|
| 25 |
+
result = create_quote.invoke({
|
| 26 |
+
"product_ids": sample_product_ids,
|
| 27 |
+
"customer_name": "Maria Rodriguez",
|
| 28 |
+
"customer_email": "maria.rodriguez@techstart.com",
|
| 29 |
+
"customer_company": "TechStart Innovations",
|
| 30 |
+
"target_currency": "EUR",
|
| 31 |
+
"notes": "Startup package deal"
|
| 32 |
+
})
|
| 33 |
+
|
| 34 |
+
print("📊 Result:")
|
| 35 |
+
if result.get('success'):
|
| 36 |
+
print("✅ Quote created successfully!")
|
| 37 |
+
print(f"📄 Quote ID: {result['quote_id']}")
|
| 38 |
+
print(f"👤 Customer: {result['customer_name']}")
|
| 39 |
+
print(f"💰 Grand Total: €{result['grand_total']:,.2f}")
|
| 40 |
+
print(f"📦 Unique Products: {result['unique_products']}")
|
| 41 |
+
print(f"🔢 Total Items: {result['total_items']}")
|
| 42 |
+
print(f"📄 File Format: {result['file_format']}")
|
| 43 |
+
print(f"💾 Saved to: {result['file_path']}")
|
| 44 |
+
|
| 45 |
+
# Show quote summary
|
| 46 |
+
print("\n📊 Quote Summary:")
|
| 47 |
+
print("-" * 40)
|
| 48 |
+
for item in result['quote_summary']['items']:
|
| 49 |
+
print(f"• ID {item['product_id']}: {item['name']} (Qty: {item['quantity']}) - €{item['total']:,.2f}")
|
| 50 |
+
print("-" * 40)
|
| 51 |
+
print(f"GRAND TOTAL: €{result['quote_summary']['grand_total']:,.2f}")
|
| 52 |
+
|
| 53 |
+
# Show first 25 lines of the generated Markdown file
|
| 54 |
+
print("\n📄 Generated Markdown Quote (preview):")
|
| 55 |
+
print("-" * 60)
|
| 56 |
+
try:
|
| 57 |
+
with open(result['file_path'], 'r', encoding='utf-8') as f:
|
| 58 |
+
lines = f.readlines()
|
| 59 |
+
for i, line in enumerate(lines[:25]):
|
| 60 |
+
print(line.rstrip())
|
| 61 |
+
if len(lines) > 25:
|
| 62 |
+
print("... (truncated for preview)")
|
| 63 |
+
except Exception as e:
|
| 64 |
+
print(f"Could not read quote file: {e}")
|
| 65 |
+
|
| 66 |
+
else:
|
| 67 |
+
print("❌ Quote creation failed:")
|
| 68 |
+
print(result.get('error', 'Unknown error'))
|
| 69 |
+
|
| 70 |
+
return result
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_different_currencies():
|
| 74 |
+
"""Test quote generation with different currencies."""
|
| 75 |
+
print("\n\n💱 Testing Currency Conversion")
|
| 76 |
+
print("=" * 50)
|
| 77 |
+
|
| 78 |
+
# Test with USD as target currency
|
| 79 |
+
result = create_quote.invoke({
|
| 80 |
+
"product_ids": [760, 763, 765], # 1 of each
|
| 81 |
+
"customer_name": "John Williams",
|
| 82 |
+
"customer_company": "Global Tech Solutions",
|
| 83 |
+
"target_currency": "USD",
|
| 84 |
+
"notes": "USD pricing for US customer"
|
| 85 |
+
})
|
| 86 |
+
|
| 87 |
+
if result.get('success'):
|
| 88 |
+
print(f"✅ USD Quote: ${result['grand_total']:,.2f}")
|
| 89 |
+
print(f"📄 File: {result['file_path']}")
|
| 90 |
+
else:
|
| 91 |
+
print(f"❌ USD Quote failed: {result.get('error')}")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_error_cases():
|
| 95 |
+
"""Test error handling."""
|
| 96 |
+
print("\n\n⚠️ Testing Error Handling")
|
| 97 |
+
print("=" * 50)
|
| 98 |
+
|
| 99 |
+
# Test with invalid product IDs
|
| 100 |
+
print("Test 1: Invalid product IDs")
|
| 101 |
+
result1 = create_quote.invoke({
|
| 102 |
+
"product_ids": [999999, 999998], # Likely non-existent IDs
|
| 103 |
+
"customer_name": "Test Customer"
|
| 104 |
+
})
|
| 105 |
+
print(f"Result: {result1}")
|
| 106 |
+
|
| 107 |
+
# Test with empty product IDs
|
| 108 |
+
print("\nTest 2: Empty product IDs")
|
| 109 |
+
result2 = create_quote.invoke({
|
| 110 |
+
"product_ids": [],
|
| 111 |
+
"customer_name": "Test Customer"
|
| 112 |
+
})
|
| 113 |
+
print(f"Result: {result2}")
|
| 114 |
+
|
| 115 |
+
# Test with missing customer name
|
| 116 |
+
print("\nTest 3: Missing customer name")
|
| 117 |
+
result3 = create_quote.invoke({
|
| 118 |
+
"product_ids": [760, 763],
|
| 119 |
+
"customer_name": ""
|
| 120 |
+
})
|
| 121 |
+
print(f"Result: {result3}")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def test_sample_quote_function():
|
| 125 |
+
"""Test the sample quote function."""
|
| 126 |
+
print("\n\n🎯 Testing Sample Quote Function")
|
| 127 |
+
print("=" * 50)
|
| 128 |
+
|
| 129 |
+
result = create_sample_quote_from_ids()
|
| 130 |
+
print("Sample quote result:")
|
| 131 |
+
if result.get('success'):
|
| 132 |
+
print(f"✅ Quote ID: {result['quote_id']}")
|
| 133 |
+
print(f"💰 Total: €{result['grand_total']:,.2f}")
|
| 134 |
+
print(f"📄 File: {result['file_path']}")
|
| 135 |
+
else:
|
| 136 |
+
print(f"❌ Failed: {result.get('error')}")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
if __name__ == "__main__":
|
| 140 |
+
print("🚀 Starting Product ID-based Quote Generation Tests\n")
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
# Run all tests
|
| 144 |
+
main_result = test_product_id_quote()
|
| 145 |
+
test_different_currencies()
|
| 146 |
+
test_sample_quote_function()
|
| 147 |
+
test_error_cases()
|
| 148 |
+
|
| 149 |
+
print("\n✅ All tests completed!")
|
| 150 |
+
|
| 151 |
+
if main_result and main_result.get('success'):
|
| 152 |
+
print(f"\n🎉 Check out your generated Markdown quote at:")
|
| 153 |
+
print(f"📄 {main_result['file_path']}")
|
| 154 |
+
|
| 155 |
+
except Exception as e:
|
| 156 |
+
print(f"\n❌ Test failed with error: {e}")
|
| 157 |
+
import traceback
|
| 158 |
+
traceback.print_exc()
|
test_files/tools_test.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sales_assistant.agent_tools.describe_table import describe_table
|
| 2 |
+
from sales_assistant.agent_tools.get_samples_data import get_sample_data
|
| 3 |
+
from sales_assistant.agent_tools.execute_advanced_query import execute_advanced_query
|
| 4 |
+
from sales_assistant.agent_tools.get_distinct_values import get_distinct_values
|
| 5 |
+
from sales_assistant.agent_tools.get_product_by_criteria import search_products_by_criteria
|
| 6 |
+
from sales_assistant.agent_tools.get_table_statistics import get_table_statistics
|
| 7 |
+
|
| 8 |
+
def test_describe_table_tool():
|
| 9 |
+
"""
|
| 10 |
+
Test the describe_table tool to ensure it retrieves schema information correctly.
|
| 11 |
+
"""
|
| 12 |
+
print("\n=== Testing describe_table tool ===")
|
| 13 |
+
|
| 14 |
+
# Test default query
|
| 15 |
+
print("Test 1: Default DESCRIBE query")
|
| 16 |
+
# Since describe_table is a LangChain tool, we need to invoke it properly
|
| 17 |
+
result = describe_table.invoke({})
|
| 18 |
+
print("Result:")
|
| 19 |
+
print(result)
|
| 20 |
+
|
| 21 |
+
# Test with custom query
|
| 22 |
+
print("\nTest 2: Custom query")
|
| 23 |
+
custom_query = "SHOW TABLES FROM streamnet;"
|
| 24 |
+
result2 = describe_table.invoke({"sql_query": custom_query})
|
| 25 |
+
print("Result:")
|
| 26 |
+
print(result2)
|
| 27 |
+
|
| 28 |
+
def test_get_sample_data_tool():
|
| 29 |
+
"""
|
| 30 |
+
Test the get_sample_data tool to ensure it retrieves sample data correctly with different methods.
|
| 31 |
+
"""
|
| 32 |
+
print("\n=== Testing get_sample_data tool ===")
|
| 33 |
+
|
| 34 |
+
# Test default method (limit with default sample_size=5)
|
| 35 |
+
print("Test 1: Default method (limit, sample_size=5)")
|
| 36 |
+
result = get_sample_data.invoke({})
|
| 37 |
+
print("Result:")
|
| 38 |
+
print(result)
|
| 39 |
+
|
| 40 |
+
# Test limit method with custom sample_size
|
| 41 |
+
print("Test 2: Limit method with sample_size=3")
|
| 42 |
+
result2 = get_sample_data.invoke({
|
| 43 |
+
"sql_query": "SELECT * FROM streamnet.products_list WHERE category = 'camera'",
|
| 44 |
+
"method": "limit",
|
| 45 |
+
"sample_size": 3
|
| 46 |
+
})
|
| 47 |
+
print("Result:")
|
| 48 |
+
print(result2)
|
| 49 |
+
|
| 50 |
+
# Test random method
|
| 51 |
+
print("Test 3: Random method with sample_size=2")
|
| 52 |
+
result3 = get_sample_data.invoke({
|
| 53 |
+
"sql_query": "SELECT manufacturer, model_name FROM streamnet.products_list",
|
| 54 |
+
"method": "random",
|
| 55 |
+
"sample_size": 2
|
| 56 |
+
})
|
| 57 |
+
print("Result:")
|
| 58 |
+
print(result3)
|
| 59 |
+
|
| 60 |
+
# Test None method (all results)
|
| 61 |
+
print("Test 4: None method (all results)")
|
| 62 |
+
result4 = get_sample_data.invoke({
|
| 63 |
+
"sql_query": "SELECT manufacturer, COUNT(*) as count FROM streamnet.products_list GROUP BY manufacturer",
|
| 64 |
+
"method": None,
|
| 65 |
+
"sample_size": 10 # This is ignored
|
| 66 |
+
})
|
| 67 |
+
print("Result:")
|
| 68 |
+
print(result4)
|
| 69 |
+
|
| 70 |
+
# Test "none" string method (alternative way)
|
| 71 |
+
print("Test 5: 'none' string method (all results)")
|
| 72 |
+
result5 = get_sample_data.invoke({
|
| 73 |
+
"sql_query": "SELECT category, COUNT(*) as count FROM streamnet.products_list GROUP BY category",
|
| 74 |
+
"method": "none",
|
| 75 |
+
"sample_size": 5 # This is ignored
|
| 76 |
+
})
|
| 77 |
+
print("Result:")
|
| 78 |
+
print(result5)
|
| 79 |
+
|
| 80 |
+
def test_execute_advanced_query_tool():
|
| 81 |
+
"""
|
| 82 |
+
Test the execute_advanced_query tool to ensure it executes complex SQL queries correctly.
|
| 83 |
+
"""
|
| 84 |
+
print("\n=== Testing execute_advanced_query tool ===")
|
| 85 |
+
|
| 86 |
+
# Test basic advanced query
|
| 87 |
+
print("Test 1: Basic advanced query with aggregation")
|
| 88 |
+
query1 = "SELECT manufacturer, COUNT(*) as product_count, AVG(msrp) as avg_price FROM streamnet.products_list WHERE msrp > 0 GROUP BY manufacturer LIMIT 5"
|
| 89 |
+
result1 = execute_advanced_query.invoke({"sql_query": query1})
|
| 90 |
+
print("Result:")
|
| 91 |
+
print(result1)
|
| 92 |
+
|
| 93 |
+
# Test complex query with joins/subqueries concept
|
| 94 |
+
print("Test 2: Complex query with filtering")
|
| 95 |
+
query2 = "SELECT manufacturer, category, model_name, msrp FROM streamnet.products_list WHERE msrp = (SELECT MAX(msrp) FROM streamnet.products_list WHERE manufacturer = 'Apple') LIMIT 3"
|
| 96 |
+
result2 = execute_advanced_query.invoke({"sql_query": query2})
|
| 97 |
+
print("Result:")
|
| 98 |
+
print(result2)
|
| 99 |
+
|
| 100 |
+
def test_get_distinct_values_tool():
|
| 101 |
+
"""
|
| 102 |
+
Test the get_distinct_values tool to ensure it retrieves distinct values and patterns correctly.
|
| 103 |
+
"""
|
| 104 |
+
print("\n=== Testing get_distinct_values tool ===")
|
| 105 |
+
|
| 106 |
+
# Test default query
|
| 107 |
+
print("Test 1: Default query (manufacturers by count)")
|
| 108 |
+
result1 = get_distinct_values.invoke({})
|
| 109 |
+
print("Result:")
|
| 110 |
+
print(result1)
|
| 111 |
+
|
| 112 |
+
# Test custom query for categories
|
| 113 |
+
print("Test 2: Custom query for categories")
|
| 114 |
+
query2 = "SELECT category, COUNT(*) as count FROM streamnet.products_list GROUP BY category ORDER BY count DESC LIMIT 5"
|
| 115 |
+
result2 = get_distinct_values.invoke({"sql_query": query2})
|
| 116 |
+
print("Result:")
|
| 117 |
+
print(result2)
|
| 118 |
+
|
| 119 |
+
# Test distinct model names
|
| 120 |
+
print("Test 3: Distinct model names for specific category")
|
| 121 |
+
query3 = "SELECT DISTINCT model_name FROM streamnet.products_list WHERE category = 'camera' ORDER BY model_name LIMIT 5"
|
| 122 |
+
result3 = get_distinct_values.invoke({"sql_query": query3})
|
| 123 |
+
print("Result:")
|
| 124 |
+
print(result3)
|
| 125 |
+
|
| 126 |
+
def test_search_products_by_criteria_tool():
|
| 127 |
+
"""
|
| 128 |
+
Test the search_products_by_criteria tool to ensure it searches products correctly.
|
| 129 |
+
"""
|
| 130 |
+
print("\n=== Testing search_products_by_criteria tool ===")
|
| 131 |
+
|
| 132 |
+
# Test default query
|
| 133 |
+
print("Test 1: Default query (Samsung products)")
|
| 134 |
+
result1 = search_products_by_criteria.invoke({})
|
| 135 |
+
print("Result:")
|
| 136 |
+
print(result1)
|
| 137 |
+
|
| 138 |
+
# Test custom search with price range
|
| 139 |
+
print("Test 2: Custom search with price range")
|
| 140 |
+
query2 = "SELECT * FROM streamnet.products_list WHERE msrp BETWEEN 100 AND 500 AND manufacturer = 'Apple' LIMIT 5"
|
| 141 |
+
result2 = search_products_by_criteria.invoke({"sql_query": query2})
|
| 142 |
+
print("Result:")
|
| 143 |
+
print(result2)
|
| 144 |
+
|
| 145 |
+
# Test search with text matching
|
| 146 |
+
print("Test 3: Search with text matching")
|
| 147 |
+
query3 = "SELECT manufacturer, model_name, msrp FROM streamnet.products_list WHERE model_name LIKE '%Pro%' ORDER BY msrp DESC LIMIT 5"
|
| 148 |
+
result3 = search_products_by_criteria.invoke({"sql_query": query3})
|
| 149 |
+
print("Result:")
|
| 150 |
+
print(result3)
|
| 151 |
+
|
| 152 |
+
def test_get_table_statistics_tool():
|
| 153 |
+
"""
|
| 154 |
+
Test the get_table_statistics tool to ensure it generates statistics correctly.
|
| 155 |
+
"""
|
| 156 |
+
print("\n=== Testing get_table_statistics tool ===")
|
| 157 |
+
|
| 158 |
+
# Test default query
|
| 159 |
+
print("Test 1: Default query (basic table statistics)")
|
| 160 |
+
result1 = get_table_statistics.invoke({})
|
| 161 |
+
print("Result:")
|
| 162 |
+
print(result1)
|
| 163 |
+
|
| 164 |
+
# Test price statistics
|
| 165 |
+
print("Test 2: Price statistics")
|
| 166 |
+
query2 = "SELECT COUNT(*) as total_products, AVG(msrp) as avg_price, MIN(msrp) as min_price, MAX(msrp) as max_price FROM streamnet.products_list WHERE msrp > 0"
|
| 167 |
+
result2 = get_table_statistics.invoke({"sql_query": query2})
|
| 168 |
+
print("Result:")
|
| 169 |
+
print(result2)
|
| 170 |
+
|
| 171 |
+
# Test manufacturer statistics
|
| 172 |
+
print("Test 3: Manufacturer statistics")
|
| 173 |
+
query3 = "SELECT manufacturer, COUNT(*) as product_count, AVG(msrp) as avg_price FROM streamnet.products_list WHERE msrp > 0 GROUP BY manufacturer ORDER BY product_count DESC LIMIT 5"
|
| 174 |
+
result3 = get_table_statistics.invoke({"sql_query": query3})
|
| 175 |
+
print("Result:")
|
| 176 |
+
print(result3)
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
test_describe_table_tool()
|
| 180 |
+
test_get_sample_data_tool()
|
| 181 |
+
test_execute_advanced_query_tool()
|
| 182 |
+
test_get_distinct_values_tool()
|
| 183 |
+
test_search_products_by_criteria_tool()
|
| 184 |
+
test_get_table_statistics_tool()
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|