krinya commited on
Commit
6bd3e57
·
0 Parent(s):

Refactor code structure and remove redundant sections for improved readability and maintainability

Browse files
Files changed (33) hide show
  1. .github/copilot-instructions.md +12 -0
  2. .gitignore +94 -0
  3. README.md +21 -0
  4. pyproject.toml +35 -0
  5. src/sales_assistant/__init__.py +1 -0
  6. src/sales_assistant/agent_main/__init__.py +1 -0
  7. src/sales_assistant/agent_main/agent_graph.py +49 -0
  8. src/sales_assistant/agent_main/agent_node.py +71 -0
  9. src/sales_assistant/agent_main/agent_runner.py +262 -0
  10. src/sales_assistant/agent_main/tools_node.py +43 -0
  11. src/sales_assistant/agent_tools/__init__.py +1 -0
  12. src/sales_assistant/agent_tools/agent_tools_utils.py +69 -0
  13. src/sales_assistant/agent_tools/create_quote.py +348 -0
  14. src/sales_assistant/agent_tools/describe_table.py +36 -0
  15. src/sales_assistant/agent_tools/execute_advanced_query.py +37 -0
  16. src/sales_assistant/agent_tools/execute_sql_query.py +79 -0
  17. src/sales_assistant/agent_tools/get_distinct_values.py +37 -0
  18. src/sales_assistant/agent_tools/get_exchange_rates.py +66 -0
  19. src/sales_assistant/agent_tools/get_product_by_criteria.py +36 -0
  20. src/sales_assistant/agent_tools/get_samples_data.py +57 -0
  21. src/sales_assistant/agent_tools/get_table_statistics.py +36 -0
  22. src/sales_assistant/agent_tools/quote_template.py +242 -0
  23. src/sales_assistant/created_quotes/quote_QT-B4849588_Kristof_Proba_20250825_114359.md +63 -0
  24. src/sales_assistant/db/utils/db_connections.py +105 -0
  25. src/sales_assistant/db/utils/db_utils.py +75 -0
  26. src/sales_assistant/main.py +226 -0
  27. src/sales_assistant/prompts/__init__.py +0 -0
  28. src/sales_assistant/prompts/system_prompt.py +141 -0
  29. test_files/db_test.py +81 -0
  30. test_files/quote_test.py +171 -0
  31. test_files/test_product_id_quotes.py +158 -0
  32. test_files/tools_test.py +184 -0
  33. 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