From 173c767fb48a93412e2ade864d1917df473a293b Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Thu, 30 Oct 2025 11:49:25 -0500 Subject: [PATCH 1/5] Adding .venv as an ignored directory in .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b01d78f..fbc1b1f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ venv/ env/ backend/backend/ +.venv/ # Node modules node_modules/ From 466cb88e9a040005b06303acbc9acb2f302081a0 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Thu, 30 Oct 2025 23:12:06 -0500 Subject: [PATCH 2/5] Updates to the schema for chat data model --- backend/chat_data_model.py | 153 ++++++------------------------------- 1 file changed, 23 insertions(+), 130 deletions(-) diff --git a/backend/chat_data_model.py b/backend/chat_data_model.py index 4ad3106..e800c3d 100644 --- a/backend/chat_data_model.py +++ b/backend/chat_data_model.py @@ -6,7 +6,6 @@ from shared.utils import get_user_id # Global variables that will be set by the main app db = None -ChatHistory = None Threads = None Runs = None Users = None @@ -16,7 +15,7 @@ def init_chat_db(database): """Initialize the database reference and create models""" - global db, ChatHistory, Threads, Runs, Users, ToolCalls, ToolDefinition, ChatHistoryManager, AgentDefinition + global db, Threads, Runs, Users, ToolCalls, ToolDefinition, ChatHistoryManager, AgentDefinition db = database # Helper function to convert model instances to dictionaries @@ -108,35 +107,6 @@ class ToolCalls(db.Model): def to_dict(self): return to_dict_helper(self) - class ChatHistory(db.Model): - __tablename__ = 'chat_history' - message_id = db.Column(db.String(255), primary_key=True, default=lambda: f"msg_{uuid.uuid4()}") - session_id = db.Column(db.BigInteger, db.ForeignKey('threads.thread_id')) - trace_id = db.Column(db.String(255), nullable=False) - user_id = db.Column(db.String(255), nullable=False) - agent_id = db.Column(db.BigInteger, nullable=True) - message_type = db.Column(db.String(50), nullable=False) # 'human', 'ai', 'system', 'tool_call', 'tool_result' - content = db.Column(db.Text) - - model_name = db.Column(db.String(255)) - content_filter_results = db.Column(db.JSON) - total_tokens = db.Column(db.Integer) - completion_tokens = db.Column(db.Integer) - prompt_tokens = db.Column(db.Integer) - - tool_id = db.Column(db.String(255)) - tool_name = db.Column(db.String(255)) - tool_input = db.Column(db.JSON) - tool_output = db.Column(db.JSON) - tool_call_id = db.Column(db.String(255)) - - finish_reason = db.Column(db.String(255)) - response_time_ms = db.Column(db.Integer) - trace_end = db.Column(db.DateTime, default=datetime.now()) - - def to_dict(self): - return to_dict_helper(self) - # --- Chat History Management Class --- class ChatHistoryManager: def __init__(self, session_id: str, user_id: str = 'user_1'): @@ -182,73 +152,22 @@ def add_trace_messages(self, serialized_messages: str, return res def add_human_message(self, message: dict, trace_id: str): """Add the human message to chat history""" - entry_message = ChatHistory( - session_id=self.session_id, - user_id=self.user_id, - trace_id=trace_id, - message_id = str(uuid.uuid4()), - message_type="human", - content=message['content'], - ) - db.session.add(entry_message) - db.session.commit() - print("Human message added to chat history:", message["id"]) - return entry_message + # ChatHistory table removed - method now does nothing + print("Human message would be added to chat history:", message["id"]) + return None def add_ai_message(self, message: dict, trace_id: str, trace_duration: int): """Add the AI agent message to chat history""" - agent_id = None - if "name" in message: - agent_id = db.session.query(AgentDefinition.agent_id).filter_by(name=message["name"]).scalar() - entry_message = ChatHistory( - session_id=self.session_id, - user_id=self.user_id, - agent_id = agent_id, - message_id = message["id"], - trace_id=trace_id, - message_type="ai", - content=message["content"], - total_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('total_tokens'), - completion_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('completion_tokens'), - prompt_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('prompt_tokens'), - model_name=message.get("response_metadata", {}).get('model_name'), - content_filter_results=message.get("response_metadata", {}).get("prompt_filter_results", [{}])[0].get("content_filter_results"), - finish_reason=message.get("response_metadata", {}).get("finish_reason"), - response_time_ms=trace_duration, - ) - db.session.add(entry_message) - db.session.commit() - print("AI message added to chat history:", message["id"]) - return entry_message + # ChatHistory table removed - method now does nothing + print("AI message would be added to chat history:", message["id"]) + return None def add_tool_call_message(self, message: dict, trace_id: str): """Log a tool call""" - agent_id = None - if "name" in message: - agent_id = db.session.query(AgentDefinition.agent_id).filter_by(name=message["name"]).scalar() tool_name = message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('function', {}).get("name") tool_id = db.session.query(ToolDefinition.tool_id).filter_by(name=tool_name).scalar() - - entry_message = ChatHistory( - session_id=self.session_id, - user_id=self.user_id, - agent_id=agent_id, - trace_id=trace_id, - message_type='tool_call', - tool_id = tool_id, - tool_call_id=message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('id'), - tool_name=tool_name, - total_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('total_tokens'), - completion_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('completion_tokens'), - prompt_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('prompt_tokens'), - tool_input=message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('function', {}).get("arguments"), - model_name=message.get("response_metadata", {}).get('model_name'), - content_filter_results=message.get("response_metadata", {}).get("prompt_filter_results", [{}])[0].get("content_filter_results"), - finish_reason=message.get("response_metadata", {}).get("finish_reason"), - ) - db.session.add(entry_message) - db.session.commit() - print("Tool call message added to chat history:", message["id"]) + + print("Tool call message would be added to chat history:", message["id"]) return {"tool_call_id": message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('id'), "tool_id": tool_id, "tool_name": tool_name, "tool_input": message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('function', {}).get("arguments"), @@ -256,23 +175,8 @@ def add_tool_call_message(self, message: dict, trace_id: str): def add_tool_result_message(self, message: dict, trace_id: str): """Log a tool result""" - tool_name = message["name"] - tool_id = db.session.query(ToolDefinition.tool_id).filter_by(name=tool_name).scalar() - entry_message = ChatHistory( - session_id=self.session_id, - user_id=self.user_id, - message_id=message["id"], - tool_id=tool_id, - tool_call_id=message["tool_call_id"], - trace_id=trace_id, - tool_name=message["name"], - message_type='tool_result', - content="", - tool_output=message["content"], - ) - db.session.add(entry_message) - db.session.commit() - print("Tool result message added to chat history:", message["id"]) + # ChatHistory table removed - method now does nothing + print("Tool result message would be added to chat history:", message["id"]) return {"tool_output": message["content"], "status": message["status"]} def update_session_timestamp(self): @@ -327,15 +231,10 @@ def log_tool_usage(self, tool_info: dict, trace_id: str): def get_conversation_history(self, limit: int = 50): """Retrieve conversation history for this session""" - messages = db.session.query(ChatHistory.trace_id, ChatHistory.message_type, ChatHistory.content, ChatHistory.trace_end).filter_by( - session_id=self.session_id - ).order_by(ChatHistory.trace_end.desc()).limit(limit).all() - - # Note: messages will be tuples, not model objects, so you'll need to handle differently - return [{"trace_id": msg[0], "message_type": msg[1], "content": msg[2], "trace_end": msg[3]} for msg in reversed(messages)] + # ChatHistory table removed - return empty list + return [] # Make classes available globally in this module - globals()['ChatHistory'] = ChatHistory globals()['Threads'] = Threads globals()['Runs'] = Runs globals()['Users'] = Users @@ -370,7 +269,6 @@ def clear_chat_history(): try: # Delete in order to respect foreign key constraints ToolCalls.query.delete() - ChatHistory.query.delete() Threads.query.delete() db.session.commit() @@ -385,7 +283,6 @@ def clear_session_data(session_id): try: # Delete in order to respect foreign key constraints ToolCalls.query.filter_by(session_id=session_id).delete() - ChatHistory.query.filter_by(session_id=session_id).delete() Threads.query.filter_by(thread_id=session_id).delete() db.session.commit() @@ -400,35 +297,33 @@ def initialize_tool_definitions(): { "name": "get_user_accounts", "description": "Retrieves all accounts for a given user", - "input_schema": {"type": "object", "properties": {}}, - "cost_per_call_cents": 0 + "input_schema": json.dumps({"type": "object", "properties": {}}) }, { "name": "get_transactions_summary", "description": "Provides spending summary with time period and account filters", - "input_schema": { + "input_schema": json.dumps({ "type": "object", "properties": { "time_period": {"type": "string"}, "account_name": {"type": "string"} } - }, - "cost_per_call_cents": 0 + }) }, { "name": "search_support_documents", "description": "Searches knowledge base for customer support answers", - "input_schema": { + "input_schema": json.dumps({ "type": "object", "properties": {"user_question": {"type": "string"}}, "required": ["user_question"] - }, - "cost_per_call_cents": 2 + }) + }, { "name": "create_new_account", "description": "Creates a new bank account for the user", - "input_schema": { + "input_schema": json.dumps({ "type": "object", "properties": { "account_type": {"type": "string", "enum": ["checking", "savings", "credit"]}, @@ -436,13 +331,12 @@ def initialize_tool_definitions(): "balance": {"type": "number"} }, "required": ["account_type", "name"] - }, - "cost_per_call_cents": 0 + }) }, { "name": "transfer_money", "description": "Transfers money between accounts or to external accounts", - "input_schema": { + "input_schema": json.dumps({ "type": "object", "properties": { "from_account_name": {"type": "string"}, @@ -451,8 +345,7 @@ def initialize_tool_definitions(): "to_external_details": {"type": "object"} }, "required": ["from_account_name", "amount"] - }, - "cost_per_call_cents": 0 + }) } ] From 5849c6121d689329d75c1f7731605c2e255abf33 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Wed, 5 Nov 2025 15:12:20 -0600 Subject: [PATCH 3/5] Resetting back to the original data model Reset chat data model to replace Threads, Runs, Users, and ToolCalls with ChatSession, ChatHistory, and ToolUsage. Update database models and methods for chat session management and history logging. --- backend/chat_data_model.py | 272 ++++++++++++++++++++++++------------- 1 file changed, 176 insertions(+), 96 deletions(-) diff --git a/backend/chat_data_model.py b/backend/chat_data_model.py index e800c3d..7b2a359 100644 --- a/backend/chat_data_model.py +++ b/backend/chat_data_model.py @@ -6,16 +6,15 @@ from shared.utils import get_user_id # Global variables that will be set by the main app db = None -Threads = None -Runs = None -Users = None -ToolCalls = None +ChatHistory = None +ChatSession = None +ToolUsage = None ToolDefinition = None ChatHistoryManager = None def init_chat_db(database): """Initialize the database reference and create models""" - global db, Threads, Runs, Users, ToolCalls, ToolDefinition, ChatHistoryManager, AgentDefinition + global db, ChatHistory, ChatSession, ToolUsage, ToolDefinition, ChatHistoryManager, AgentDefinition db = database # Helper function to convert model instances to dictionaries @@ -31,9 +30,8 @@ def to_dict_helper(instance): class AgentDefinition(db.Model): __tablename__ = 'agent_definitions' - agent_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) + agent_id = db.Column(db.String(255), primary_key=True, default=lambda: f"agent_{uuid.uuid4()}") name = db.Column(db.String(255), unique=True, nullable=False) - version = db.Column(db.NCHAR(10)) description = db.Column(db.Text) llm_config = db.Column(db.JSON, nullable=False) prompt_template = db.Column(db.Text, nullable=False) @@ -41,68 +39,75 @@ class AgentDefinition(db.Model): def to_dict(self): return to_dict_helper(self) - class Threads(db.Model): - __tablename__ = 'threads' - thread_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) - user_id = db.Column(db.Integer, nullable=False) - agent_id = db.Column(db.BigInteger, nullable=True) + class ChatSession(db.Model): + __tablename__ = 'chat_sessions' + session_id = db.Column(db.String(255), primary_key=True, default=lambda: f"session_{uuid.uuid4()}") + user_id = db.Column(db.String(255), nullable=False) + title = db.Column(db.String(500)) created_at = db.Column(db.DateTime, default=datetime.now()) updated_at = db.Column(db.DateTime, default=datetime.now(), onupdate=datetime.now()) def to_dict(self): return to_dict_helper(self) - - class Runs(db.Model): - __tablename__ = 'runs' - run_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) - thread_id = db.Column(db.BigInteger, db.ForeignKey('threads.thread_id'), nullable=False) - input = db.Column(db.Text, nullable=False) - output = db.Column(db.Text, nullable=False) - start_time = db.Column(db.DateTime, nullable=False) - end_time = db.Column(db.DateTime, nullable=True) - status = db.Column(db.String(10), nullable=True) - total_tokens = db.Column(db.Integer, nullable=True) - input_tokens = db.Column(db.Integer, nullable=True) - output_tokens = db.Column(db.Integer, nullable=True) - - def to_dict(self): - return to_dict_helper(self) class ToolDefinition(db.Model): __tablename__ = 'tool_definitions' - tool_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) - name = db.Column(db.String(255), nullable=False) - description = db.Column(db.Text, nullable=True) - input_schema = db.Column(db.Text, nullable=False) - version = db.Column(db.String(50), nullable=True) - is_active = db.Column(db.Boolean, nullable=True) - created_at = db.Column(db.DateTime, nullable=True) - updated_at = db.Column(db.DateTime, nullable=True) + tool_id = db.Column(db.String(255), primary_key=True, default=lambda: f"tooldef_{uuid.uuid4()}") + name = db.Column(db.String(255), unique=True, nullable=False) + description = db.Column(db.Text) + input_schema = db.Column(db.JSON, nullable=False) + version = db.Column(db.String(50), default='1.0.0') + is_active = db.Column(db.Boolean, default=True) + cost_per_call_cents = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=datetime.now()) + updated_at = db.Column(db.DateTime, default=datetime.now(), onupdate=datetime.now()) def to_dict(self): return to_dict_helper(self) - - class Users(db.Model): - __tablename__ = 'users' - user_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) - user_guid = db.Column(db.String(255), nullable=False) - description = db.Column(db.String(500), nullable=True) - user_name = db.Column(db.String(500), nullable=False) + + class ToolUsage(db.Model): + __tablename__ = 'tool_usage' + tool_call_id = db.Column(db.String(255), primary_key=True, default=lambda: f"tool_{uuid.uuid4()}") + session_id = db.Column(db.String(255), nullable=False) + trace_id = db.Column(db.String(255), db.ForeignKey('chat_history.trace_id')) + tool_id = db.Column(db.String(255), db.ForeignKey('tool_definitions.tool_id'), nullable=False) + tool_name = db.Column(db.String(255), nullable=False) + tool_input = db.Column(db.JSON, nullable=False) + tool_output = db.Column(db.JSON) + tool_message = db.Column(db.Text) + status = db.Column(db.String(50)) + + # Additional tracking fields + tokens_used = db.Column(db.Integer) def to_dict(self): return to_dict_helper(self) - - class ToolCalls(db.Model): - __tablename__ = 'tool_calls' - tool_call_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) - tool_id = db.Column(db.BigInteger, nullable=False) - run_id = db.Column(db.BigInteger, db.ForeignKey('runs.run_id'), nullable=False) - start_time = db.Column(db.DateTime, nullable=True) - end_time = db.Column(db.DateTime, nullable=True) - status = db.Column(db.String(50), nullable=True) - attributes = db.Column(db.Text, nullable=True) - input = db.Column(db.Text, nullable=True) - output = db.Column(db.Text, nullable=True) + + class ChatHistory(db.Model): + __tablename__ = 'chat_history' + message_id = db.Column(db.String(255), primary_key=True, default=lambda: f"msg_{uuid.uuid4()}") + session_id = db.Column(db.String(255), db.ForeignKey('chat_sessions.session_id')) + trace_id = db.Column(db.String(255), nullable=False) + user_id = db.Column(db.String(255), nullable=False) + agent_id = db.Column(db.String(255), nullable=True) + message_type = db.Column(db.String(50), nullable=False) # 'human', 'ai', 'system', 'tool_call', 'tool_result' + content = db.Column(db.Text) + + model_name = db.Column(db.String(255)) + content_filter_results = db.Column(db.JSON) + total_tokens = db.Column(db.Integer) + completion_tokens = db.Column(db.Integer) + prompt_tokens = db.Column(db.Integer) + + tool_id = db.Column(db.String(255)) + tool_name = db.Column(db.String(255)) + tool_input = db.Column(db.JSON) + tool_output = db.Column(db.JSON) + tool_call_id = db.Column(db.String(255)) + + finish_reason = db.Column(db.String(255)) + response_time_ms = db.Column(db.Integer) + trace_end = db.Column(db.DateTime, default=datetime.now()) def to_dict(self): return to_dict_helper(self) @@ -116,14 +121,14 @@ def __init__(self, session_id: str, user_id: str = 'user_1'): def _ensure_session_exists(self): """Ensure the chat session exists in the database""" - session = Threads.query.filter_by(thread_id=self.session_id).first() + session = ChatSession.query.filter_by(session_id=self.session_id).first() if not session: - session = Threads( - thread_id=self.session_id, + session = ChatSession( + session_id=self.session_id, title= "New Session", user_id=self.user_id, ) - print("-----------------> New chat session created: ", session.thread_id) + print("-----------------> New chat session created: ", session.session_id) db.session.add(session) db.session.commit() def add_trace_messages(self, serialized_messages: str, @@ -152,22 +157,73 @@ def add_trace_messages(self, serialized_messages: str, return res def add_human_message(self, message: dict, trace_id: str): """Add the human message to chat history""" - # ChatHistory table removed - method now does nothing - print("Human message would be added to chat history:", message["id"]) - return None + entry_message = ChatHistory( + session_id=self.session_id, + user_id=self.user_id, + trace_id=trace_id, + message_id = str(uuid.uuid4()), + message_type="human", + content=message['content'], + ) + db.session.add(entry_message) + db.session.commit() + print("Human message added to chat history:", message["id"]) + return entry_message def add_ai_message(self, message: dict, trace_id: str, trace_duration: int): """Add the AI agent message to chat history""" - # ChatHistory table removed - method now does nothing - print("AI message would be added to chat history:", message["id"]) - return None + agent_id = None + if "name" in message: + agent_id = db.session.query(AgentDefinition.agent_id).filter_by(name=message["name"]).scalar() + entry_message = ChatHistory( + session_id=self.session_id, + user_id=self.user_id, + agent_id = agent_id, + message_id = message["id"], + trace_id=trace_id, + message_type="ai", + content=message["content"], + total_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('total_tokens'), + completion_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('completion_tokens'), + prompt_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('prompt_tokens'), + model_name=message.get("response_metadata", {}).get('model_name'), + content_filter_results=message.get("response_metadata", {}).get("prompt_filter_results", [{}])[0].get("content_filter_results"), + finish_reason=message.get("response_metadata", {}).get("finish_reason"), + response_time_ms=trace_duration, + ) + db.session.add(entry_message) + db.session.commit() + print("AI message added to chat history:", message["id"]) + return entry_message def add_tool_call_message(self, message: dict, trace_id: str): """Log a tool call""" + agent_id = None + if "name" in message: + agent_id = db.session.query(AgentDefinition.agent_id).filter_by(name=message["name"]).scalar() tool_name = message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('function', {}).get("name") tool_id = db.session.query(ToolDefinition.tool_id).filter_by(name=tool_name).scalar() - - print("Tool call message would be added to chat history:", message["id"]) + + entry_message = ChatHistory( + session_id=self.session_id, + user_id=self.user_id, + agent_id=agent_id, + trace_id=trace_id, + message_type='tool_call', + tool_id = tool_id, + tool_call_id=message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('id'), + tool_name=tool_name, + total_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('total_tokens'), + completion_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('completion_tokens'), + prompt_tokens=message.get("response_metadata", {}).get("token_usage", {}).get('prompt_tokens'), + tool_input=message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('function', {}).get("arguments"), + model_name=message.get("response_metadata", {}).get('model_name'), + content_filter_results=message.get("response_metadata", {}).get("prompt_filter_results", [{}])[0].get("content_filter_results"), + finish_reason=message.get("response_metadata", {}).get("finish_reason"), + ) + db.session.add(entry_message) + db.session.commit() + print("Tool call message added to chat history:", message["id"]) return {"tool_call_id": message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('id'), "tool_id": tool_id, "tool_name": tool_name, "tool_input": message.get("additional_kwargs", {}).get('tool_calls', [{}])[0].get('function', {}).get("arguments"), @@ -175,13 +231,28 @@ def add_tool_call_message(self, message: dict, trace_id: str): def add_tool_result_message(self, message: dict, trace_id: str): """Log a tool result""" - # ChatHistory table removed - method now does nothing - print("Tool result message would be added to chat history:", message["id"]) + tool_name = message["name"] + tool_id = db.session.query(ToolDefinition.tool_id).filter_by(name=tool_name).scalar() + entry_message = ChatHistory( + session_id=self.session_id, + user_id=self.user_id, + message_id=message["id"], + tool_id=tool_id, + tool_call_id=message["tool_call_id"], + trace_id=trace_id, + tool_name=message["name"], + message_type='tool_result', + content="", + tool_output=message["content"], + ) + db.session.add(entry_message) + db.session.commit() + print("Tool result message added to chat history:", message["id"]) return {"tool_output": message["content"], "status": message["status"]} def update_session_timestamp(self): """Update the session's updated_at timestamp""" - session = Threads.query.filter_by(thread_id=self.session_id).first() + session = ChatSession.query.filter_by(session_id=self.session_id).first() if session: session.updated_at = datetime.now() db.session.commit() @@ -189,7 +260,7 @@ def update_session_timestamp(self): def log_tool_usage(self, tool_info: dict, trace_id: str): """Log detailed tool usage metrics""" - existing = ToolCalls.query.filter_by(tool_call_id=tool_info.get("tool_call_id")).first() + existing = ToolUsage.query.filter_by(tool_call_id=tool_info.get("tool_call_id")).first() tool_msg = '' if(type(tool_info.get("tool_output")) is dict): tool_msg = tool_info.get("tool_output").get('message', '') @@ -213,7 +284,7 @@ def log_tool_usage(self, tool_info: dict, trace_id: str): existing.tokens_used = tool_info.get("total_tokens") db.session.commit() else: - tool_usage = ToolCalls( + tool_usage = ToolUsage( session_id=self.session_id, trace_id=trace_id, tool_call_id=tool_info.get("tool_call_id"), @@ -231,14 +302,17 @@ def log_tool_usage(self, tool_info: dict, trace_id: str): def get_conversation_history(self, limit: int = 50): """Retrieve conversation history for this session""" - # ChatHistory table removed - return empty list - return [] + messages = db.session.query(ChatHistory.trace_id, ChatHistory.message_type, ChatHistory.content, ChatHistory.trace_end).filter_by( + session_id=self.session_id + ).order_by(ChatHistory.trace_end.desc()).limit(limit).all() + + # Note: messages will be tuples, not model objects, so you'll need to handle differently + return [{"trace_id": msg[0], "message_type": msg[1], "content": msg[2], "trace_end": msg[3]} for msg in reversed(messages)] # Make classes available globally in this module - globals()['Threads'] = Threads - globals()['Runs'] = Runs - globals()['Users'] = Users - globals()['ToolCalls'] = ToolCalls + globals()['ChatHistory'] = ChatHistory + globals()['ChatSession'] = ChatSession + globals()['ToolUsage'] = ToolUsage globals()['ToolDefinition'] = ToolDefinition globals()['ChatHistoryManager'] = ChatHistoryManager @@ -248,13 +322,13 @@ def handle_chat_sessions(request): user_id = get_user_id() # In production, get from auth if request.method == 'GET': - sessions = Threads.query.filter_by(user_id=user_id).order_by(Threads.updated_at.desc()).all() + sessions = ChatSession.query.filter_by(user_id=user_id).order_by(ChatSession.updated_at.desc()).all() return jsonify([session.to_dict() for session in sessions]) if request.method == 'POST': data = request.json - session = Threads( - thread_id = data.get('thread_id'), + session = ChatSession( + session_id = data.get('session_id'), user_id=user_id, title=data.get('title', 'New Chat Session'), ) @@ -268,8 +342,9 @@ def clear_chat_history(): """Clear all chat history data - USE WITH CAUTION""" try: # Delete in order to respect foreign key constraints - ToolCalls.query.delete() - Threads.query.delete() + ToolUsage.query.delete() + ChatHistory.query.delete() + ChatSession.query.delete() db.session.commit() return jsonify({"message": "All chat history cleared successfully"}), 200 @@ -282,8 +357,9 @@ def clear_session_data(session_id): """Clear chat history for a specific session""" try: # Delete in order to respect foreign key constraints - ToolCalls.query.filter_by(session_id=session_id).delete() - Threads.query.filter_by(thread_id=session_id).delete() + ToolUsage.query.filter_by(session_id=session_id).delete() + ChatHistory.query.filter_by(session_id=session_id).delete() + ChatSession.query.filter_by(session_id=session_id).delete() db.session.commit() return jsonify({"message": f"Session {session_id} data cleared successfully"}), 200 @@ -297,33 +373,35 @@ def initialize_tool_definitions(): { "name": "get_user_accounts", "description": "Retrieves all accounts for a given user", - "input_schema": json.dumps({"type": "object", "properties": {}}) + "input_schema": {"type": "object", "properties": {}}, + "cost_per_call_cents": 0 }, { "name": "get_transactions_summary", "description": "Provides spending summary with time period and account filters", - "input_schema": json.dumps({ + "input_schema": { "type": "object", "properties": { "time_period": {"type": "string"}, "account_name": {"type": "string"} } - }) + }, + "cost_per_call_cents": 0 }, { "name": "search_support_documents", "description": "Searches knowledge base for customer support answers", - "input_schema": json.dumps({ + "input_schema": { "type": "object", "properties": {"user_question": {"type": "string"}}, "required": ["user_question"] - }) - + }, + "cost_per_call_cents": 2 }, { "name": "create_new_account", "description": "Creates a new bank account for the user", - "input_schema": json.dumps({ + "input_schema": { "type": "object", "properties": { "account_type": {"type": "string", "enum": ["checking", "savings", "credit"]}, @@ -331,12 +409,13 @@ def initialize_tool_definitions(): "balance": {"type": "number"} }, "required": ["account_type", "name"] - }) + }, + "cost_per_call_cents": 0 }, { "name": "transfer_money", "description": "Transfers money between accounts or to external accounts", - "input_schema": json.dumps({ + "input_schema": { "type": "object", "properties": { "from_account_name": {"type": "string"}, @@ -345,7 +424,8 @@ def initialize_tool_definitions(): "to_external_details": {"type": "object"} }, "required": ["from_account_name", "amount"] - }) + }, + "cost_per_call_cents": 0 } ] @@ -379,4 +459,4 @@ def initialize_agent_definitions(): agent_def = AgentDefinition(**agent) db.session.add(agent_def) - db.session.commit() \ No newline at end of file + db.session.commit() From 75174de443c5e3f5a633f544c6912df0e48f670b Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Fri, 7 Nov 2025 09:31:20 -0600 Subject: [PATCH 4/5] Converted bank app to Agent Framework Removed FK in the model as it was throwing errors --- backend/banking_app.py | 201 ++++++++++++++++--------------------- backend/chat_data_model.py | 2 +- 2 files changed, 88 insertions(+), 115 deletions(-) diff --git a/backend/banking_app.py b/backend/banking_app.py index 514d7e2..5409422 100644 --- a/backend/banking_app.py +++ b/backend/banking_app.py @@ -1,5 +1,6 @@ # import urllib.parse import uuid +import asyncio from datetime import datetime import json import time @@ -11,19 +12,18 @@ from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy from dotenv import load_dotenv -from langchain_openai import AzureOpenAIEmbeddings, AzureChatOpenAI -from langchain_community.vectorstores.utils import DistanceStrategy -from langchain_sqlserver import SQLServer_VectorStore -from langchain_core.messages import HumanMessage, AIMessage, ToolMessage -from langgraph.store.memory import InMemoryStore +from agent_framework import ChatAgent +from agent_framework.azure import AzureOpenAIChatClient +from openai import AsyncOpenAI from shared.connection_manager import sqlalchemy_connection_creator, connection_manager from shared.utils import get_user_id import requests # For calling analytics service -from langgraph.prebuilt import create_react_agent from shared.utils import _serialize_messages from init_data import check_and_ingest_data # Load Environment variables and initialize app import os +from azure.identity import AzureCliCredential + load_dotenv(override=True) app = Flask(__name__) @@ -40,23 +40,21 @@ # Analytics service URL ANALYTICS_SERVICE_URL = "http://127.0.0.1:5002" -if not all([AZURE_OPENAI_KEY, AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT, AZURE_OPENAI_EMBEDDING_DEPLOYMENT]): - print("⚠️ Warning: One or more Azure OpenAI environment variables are not set.") - ai_client = None - embeddings_client = None -else: - ai_client = AzureChatOpenAI( - azure_endpoint=AZURE_OPENAI_ENDPOINT, - api_version="2024-10-21", - api_key=AZURE_OPENAI_KEY, - azure_deployment=AZURE_OPENAI_DEPLOYMENT - ) - embeddings_client = AzureOpenAIEmbeddings( - azure_deployment=AZURE_OPENAI_EMBEDDING_DEPLOYMENT, - openai_api_version="2024-10-21", - azure_endpoint=AZURE_OPENAI_ENDPOINT, - api_key=AZURE_OPENAI_KEY, +# Initialize OpenAI client for Agent Framework +chat_client = None + +try: + + # Create chat client for Agent Framework + chat_client = AzureOpenAIChatClient( + deployment_name=os.environ["AZURE_OPENAI_DEPLOYMENT"], + endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + credential=AzureCliCredential() ) + print("✅ Agent Framework OpenAI client initialized successfully") +except Exception as e: + print(f"❌ Failed to initialize OpenAI client: {e}") + chat_client = None # Database configuration for Azure SQL (banking data) app.config['SQLALCHEMY_DATABASE_URI'] = "mssql+pyodbc://" @@ -77,15 +75,8 @@ connection_url = f"mssql+pyodbc:///?odbc_connect={connection_string}" -vector_store = None -if embeddings_client: - vector_store = SQLServer_VectorStore( - connection_string=connection_url, - table_name="DocsChunks_Embeddings", - embedding_function=embeddings_client, - embedding_length=1536, - distance_strategy=DistanceStrategy.COSINE, - ) +# Vector search will be implemented as Agent Framework tools +# Agent Framework doesn't use direct vector store integration def to_dict_helper(instance): d = {} @@ -268,21 +259,9 @@ def get_transactions_summary(user_id: str = fixed_user_id, time_period: str = 't def search_support_documents(user_question: str) -> str: """Searches the knowledge base for answers to customer support questions using vector search.""" - if not vector_store: - return "The vector store is not configured." - try: - results = vector_store.similarity_search_with_score(user_question, k=3) - relevant_docs = [doc.page_content for doc, score in results if score < 0.5] - print("-------------> ", relevant_docs) - if not relevant_docs: - return "No relevant support documents found to answer this question." - - context = "\n\n---\n\n".join(relevant_docs) - return context - - except Exception as e: - print(f"ERROR in search_support_documents: {e}") - return "An error occurred while searching for support documents." + # TODO: Implement vector search using Agent Framework tools or Azure AI Search + # For now, return a placeholder response + return f"Document search functionality is being updated. Your question about '{user_question}' has been noted. Please contact customer support for immediate assistance." def create_new_account(user_id: str = fixed_user_id, account_type: str = 'checking', name: str = None, balance: float = 0.0) -> str: """Creates a new bank account for the user.""" @@ -363,83 +342,77 @@ def handle_transactions(): return jsonify(result), status_code @app.route('/api/chatbot', methods=['POST']) def chatbot(): - if not ai_client: - return jsonify({"error": "Azure OpenAI client is not configured."}), 503 + if not chat_client: + return jsonify({"error": "Agent Framework chat client is not configured."}), 503 data = request.json messages = data.get("messages", []) session_id = data.get("session_id") user_id = fixed_user_id - # session_id_temp = "session_74a4b39c-72d9-4b30-b8b4-f317e4366e1e" - - # Fetch chat history from the analytics service - history_data = call_analytics_service(f"chat/history/{session_id}", method='GET') - - # Reconstruct messages and session memory - session_memory, historical_messages = reconstruct_messages_from_history(history_data) - - # Print debugging info - print("\n--- Context being passed to the agent ---") - print(f"History data received: {len(history_data) if history_data else 0} messages") - print(f"Historical messages reconstructed: {len(historical_messages)}") - for i, msg in enumerate(historical_messages): - print(f" {i+1}. [{msg.__class__.__name__}] {msg.content[:50]}...") - print("-----------------------------------------\n") - # Extract current user message user_message = messages[-1].get("content", "") - tools = [get_user_accounts, get_transactions_summary, - search_support_documents, create_new_account, - transfer_money] - - # Initialize banking agent - banking_agent = create_react_agent( - model=ai_client, - tools=tools, - checkpointer=session_memory, - prompt=""" - - You are a customer support agent. - - You can use the provided tools to answer user questions and perform tasks. - - If you were unable to find an answer, inform the user. - - Do not use your general knowledge to answer questions.""", - name = "banking_agent_v1" - ) - # Thread config for session management - thread_config = {"configurable": {"thread_id": session_id}} - all_messages = historical_messages + [HumanMessage(content=user_message)] - - trace_start_time = time.time() - response = banking_agent.invoke( - {"messages": all_messages}, - config=thread_config - ) - end_time = time.time() - trace_duration = int((end_time - trace_start_time) * 1000) - - print("################### TRACE RESPONSE ######################") - all_messages = response['messages'] - historical_count = len(historical_messages) - final_messages = all_messages[historical_count:] - - for msg in final_messages: - print(f"[{msg.__class__.__name__}] {msg.content}") - - analytics_data = { - "session_id": session_id, - "user_id": user_id, - "messages": _serialize_messages(final_messages), - "trace_duration": trace_duration, - } - - # calling analytics service to capture this trace - call_analytics_service("chat/log-trace", data=analytics_data) - return jsonify({ - "response": final_messages[-1].content, - "session_id": session_id, - "tools_used": [] - }) + # Run the agent asynchronously + try: + response_text = asyncio.run(run_banking_agent(user_message, session_id)) + + # Log to analytics service + analytics_data = { + "session_id": session_id, + "user_id": user_id, + "messages": [ + {"type": "human", "content": user_message}, + {"type": "ai", "content": response_text} + ], + "trace_duration": 0, # Agent Framework handles timing internally + } + + call_analytics_service("chat/log-trace", data=analytics_data) + + return jsonify({ + "response": response_text, + "session_id": session_id, + "tools_used": [] + }) + + except Exception as e: + print(f"Error in chatbot: {e}") + return jsonify({"error": "An error occurred processing your request."}), 500 + +async def run_banking_agent(user_message: str, session_id: str) -> str: + """Run the banking agent using Agent Framework""" + if not chat_client: + return "Chat client is not available." + + try: + # Create agent with tools + agent = chat_client.create_agent( + name="BankingAgent", + instructions="""You are a helpful banking customer support agent. + - Help users with their banking questions and tasks + - Use the provided tools to access account information and perform operations + - Be helpful and professional + - If you cannot find information, inform the user politely""", + tools=[ + get_user_accounts, + get_transactions_summary, + search_support_documents, + create_new_account, + transfer_money + ], + ) + + # Get or create thread for session persistence + thread = agent.get_new_thread() # You could implement session-based thread retrieval here + + # Run the agent + result = await agent.run(user_message, thread=thread) + return result.text + + except Exception as e: + print(f"Error running banking agent: {e}") + return "I apologize, but I'm having trouble processing your request right now. Please try again later." def initialize_banking_app(): """Initialize banking app when called from combined launcher.""" diff --git a/backend/chat_data_model.py b/backend/chat_data_model.py index 7b2a359..efdbec2 100644 --- a/backend/chat_data_model.py +++ b/backend/chat_data_model.py @@ -69,7 +69,7 @@ class ToolUsage(db.Model): __tablename__ = 'tool_usage' tool_call_id = db.Column(db.String(255), primary_key=True, default=lambda: f"tool_{uuid.uuid4()}") session_id = db.Column(db.String(255), nullable=False) - trace_id = db.Column(db.String(255), db.ForeignKey('chat_history.trace_id')) + trace_id = db.Column(db.String(255)) tool_id = db.Column(db.String(255), db.ForeignKey('tool_definitions.tool_id'), nullable=False) tool_name = db.Column(db.String(255), nullable=False) tool_input = db.Column(db.JSON, nullable=False) From 420510cd921d856fdf28bd0282766fa7e99ba6fd Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Mon, 10 Nov 2025 10:10:38 -0600 Subject: [PATCH 5/5] Uploading changes to migrate to agent framework --- backend/banking_app.py | 63 ------------------------------------------ 1 file changed, 63 deletions(-) diff --git a/backend/banking_app.py b/backend/banking_app.py index 5409422..6af0264 100644 --- a/backend/banking_app.py +++ b/backend/banking_app.py @@ -31,12 +31,6 @@ global fixed_user_id fixed_user_id = get_user_id() # For simplicity, using a fixed user ID -# --- Azure OpenAI Configuration --- -AZURE_OPENAI_KEY = os.getenv("AZURE_OPENAI_KEY") -AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT") -AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT") -AZURE_OPENAI_EMBEDDING_DEPLOYMENT = os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT") - # Analytics service URL ANALYTICS_SERVICE_URL = "http://127.0.0.1:5002" @@ -88,63 +82,6 @@ def to_dict_helper(instance): d[column.name] = value return d -from langgraph.checkpoint.memory import MemorySaver -from langchain_core.messages import HumanMessage, AIMessage, ToolMessage -from collections import defaultdict - -def reconstruct_messages_from_history(history_data): - """Converts DB history into LangChain message objects, sorted by trace_id and message order.""" - messages = [] - print("Reconstructing messages from history data:", history_data) - - if not history_data: - return MemorySaver(), [] - - # Group messages by trace_id - traces = defaultdict(list) - for msg_data in history_data: - trace_id = msg_data.get('trace_id') - if trace_id: - traces[trace_id].append(msg_data) - - # Sort trace_ids chronologically - sorted_trace_ids = sorted(traces.keys()) - - # Process each trace in chronological order - for trace_id in sorted_trace_ids: - trace_messages = traces[trace_id] - - # Sort messages within each trace by message type priority - message_priority = { - 'human': 1, - 'ai': 2 - } - - trace_messages.sort(key=lambda x: ( - message_priority.get(x.get('message_type'), 5), - x.get('trace_end', ''), - )) - - # Convert to LangChain message objects - for msg_data in trace_messages: - try: - message_type = msg_data.get('message_type') - content = msg_data.get('content', '') - - if message_type == 'human': - messages.append(HumanMessage(content=content)) - elif message_type == 'ai': - messages.append(AIMessage(content=content)) - - except Exception as e: - print(f"Error processing message in trace {trace_id}: {e}") - continue - - print(f"Reconstructed {len(messages)} messages from {len(sorted_trace_ids)} traces") - - # Return both the memory saver and the historical messages - return MemorySaver(), messages - # Banking Database Models class User(db.Model): __tablename__ = 'users'