11"""
2- The CosmicWorksAIAgent class encapsulates a LangChain
3- agent that can be used to answer questions about Cosmic Works
4- products, customers, and sales.
2+ Class: CosmicWorksAIAgent
3+ Description:
4+ The CosmicWorksAIAgent class creates Cosmo, an AI agent
5+ that can be used to answer questions about Cosmic Works
6+ products, customers, and sales.
57"""
68import os
79import json
8- from typing import List
9- import pymongo
10+ from pydantic import BaseModel
11+ from typing import Type , TypeVar , List
1012from dotenv import load_dotenv
11- from langchain . chat_models import AzureChatOpenAI
12- from langchain . embeddings import AzureOpenAIEmbeddings
13- from langchain . vectorstores . azure_cosmos_db import AzureCosmosDBVectorSearch
14- from langchain . schema . document import Document
15- from langchain .agents import Tool
16- from langchain .agents . agent_toolkits import create_conversational_retrieval_agent
17- from langchain . tools import StructuredTool
18- from langchain_core . messages import SystemMessage
13+ from langchain_openai import AzureChatOpenAI , AzureOpenAIEmbeddings
14+ from azure . cosmos import CosmosClient , ContainerProxy
15+ from langchain_core . prompts import ChatPromptTemplate , MessagesPlaceholder
16+ from langchain_core . tools import StructuredTool
17+ from langchain .agents . agent_toolkits import create_retriever_tool
18+ from langchain .agents import AgentExecutor , create_openai_functions_agent
19+ from models import Product , SalesOrder
20+ from retrievers import AzureCosmosDBNoSQLRetriever
1921
20- load_dotenv (".env" )
21- DB_CONNECTION_STRING = os .environ .get ("DB_CONNECTION_STRING" )
22+ T = TypeVar ('T' , bound = BaseModel )
23+
24+ # Load settings for the notebook
25+ load_dotenv ()
26+ CONNECTION_STRING = os .environ .get ("COSMOS_DB_CONNECTION_STRING" )
27+ EMBEDDINGS_DEPLOYMENT_NAME = "embeddings"
28+ COMPLETIONS_DEPLOYMENT_NAME = "completions"
2229AOAI_ENDPOINT = os .environ .get ("AOAI_ENDPOINT" )
2330AOAI_KEY = os .environ .get ("AOAI_KEY" )
24- AOAI_API_VERSION = "2023-09-01-preview"
25- COMPLETIONS_DEPLOYMENT = "completions"
26- EMBEDDINGS_DEPLOYMENT = "embeddings"
27- db = pymongo .MongoClient (DB_CONNECTION_STRING ).cosmic_works
31+ AOAI_API_VERSION = "2024-06-01"
32+
33+ # Initialize the Cosmos DB client, database and product (with vector) container
34+ client = CosmosClient .from_connection_string (CONNECTION_STRING )
35+ db = client .get_database_client ("cosmic_works" )
36+ product_v_container = db .get_container_client ("product_v" )
37+ sales_order_container = db .get_container_client ("salesOrder" )
2838
2939class CosmicWorksAIAgent :
3040 """
@@ -33,156 +43,117 @@ class CosmicWorksAIAgent:
3343 products, customers, and sales.
3444 """
3545 def __init__ (self , session_id : str ):
46+ self .session_id = session_id
3647 llm = AzureChatOpenAI (
3748 temperature = 0 ,
3849 openai_api_version = AOAI_API_VERSION ,
3950 azure_endpoint = AOAI_ENDPOINT ,
4051 openai_api_key = AOAI_KEY ,
41- azure_deployment = COMPLETIONS_DEPLOYMENT
52+ azure_deployment = COMPLETIONS_DEPLOYMENT_NAME
4253 )
43- self . embedding_model = AzureOpenAIEmbeddings (
54+ embedding_model = AzureOpenAIEmbeddings (
4455 openai_api_version = AOAI_API_VERSION ,
4556 azure_endpoint = AOAI_ENDPOINT ,
4657 openai_api_key = AOAI_KEY ,
47- azure_deployment = EMBEDDINGS_DEPLOYMENT ,
48- chunk_size = 10
58+ azure_deployment = EMBEDDINGS_DEPLOYMENT_NAME ,
59+ chunk_size = 800
4960 )
50- system_message = SystemMessage (
51- content = """
52- You are a helpful, fun and friendly sales assistant for Cosmic Works,
53- a bicycle and bicycle accessories store.
54-
61+ agent_instructions = """
62+ You are a helpful, fun and friendly sales assistant for Cosmic Works, a bicycle and bicycle accessories store.
5563 Your name is Cosmo.
56-
57- You are designed to answer questions about the products that Cosmic Works sells,
58- the customers that buy them, and the sales orders that are placed by customers.
59-
60- If you don't know the answer to a question, respond with "I don't know."
61-
64+ You are designed to answer questions about the products that Cosmic Works sells, the customers that buy them, and the sales orders that are placed by customers.
65+ If you don't know the answer to a question, respond with "I don't know."
6266 Only answer questions related to Cosmic Works products, customers, and sales orders.
63-
6467 If a question is not related to Cosmic Works products, customers, or sales orders,
6568 respond with "I only answer questions about Cosmic Works"
6669 """
70+ prompt = ChatPromptTemplate .from_messages (
71+ [
72+ ("system" , agent_instructions ),
73+ MessagesPlaceholder ("chat_history" , optional = True ),
74+ ("human" , "{input}" ),
75+ MessagesPlaceholder ("agent_scratchpad" ),
76+ ]
6777 )
68- self . agent_executor = create_conversational_retrieval_agent (
69- llm ,
70- self . __create_agent_tools () ,
71- system_message = system_message ,
72- memory_key = session_id ,
73- verbose = True
78+ products_retriever = AzureCosmosDBNoSQLRetriever (
79+ embedding_model = embedding_model ,
80+ container = product_v_container ,
81+ model = Product ,
82+ vector_field_name = "contentVector" ,
83+ num_results = 5
7484 )
75-
85+ tools = [create_retriever_tool (
86+ retriever = products_retriever ,
87+ name = "vector_search_products" ,
88+ description = "Searches Cosmic Works product information for similar products based on the question. Returns the product information in JSON format."
89+ ),
90+ StructuredTool .from_function (get_product_by_id ),
91+ StructuredTool .from_function (get_product_by_sku ),
92+ StructuredTool .from_function (get_sales_by_id )]
93+ agent = create_openai_functions_agent (llm , tools , prompt )
94+ self .agent_executor = AgentExecutor (agent = agent , tools = tools , verbose = True , return_intermediate_steps = True )
95+
7696 def run (self , prompt : str ) -> str :
7797 """
7898 Run the AI agent.
7999 """
80- result = self .agent_executor ({"input" : prompt })
100+ result = self .agent_executor . invoke ({"input" : prompt })
81101 return result ["output" ]
82102
83- def __create_cosmic_works_vector_store_retriever (
84- self ,
85- collection_name : str ,
86- top_k : int = 3
87- ):
88- """
89- Returns a vector store retriever for the given collection.
90- """
91- vector_store = AzureCosmosDBVectorSearch .from_connection_string (
92- connection_string = DB_CONNECTION_STRING ,
93- namespace = f"cosmic_works.{ collection_name } " ,
94- embedding = self .embedding_model ,
95- index_name = "VectorSearchIndex" ,
96- embedding_key = "contentVector" ,
97- text_key = "_id"
98- )
99- return vector_store .as_retriever (search_kwargs = {"k" : top_k })
100-
101- def __create_agent_tools (self ) -> List [Tool ]:
102- """
103- Returns a list of agent tools.
104- """
105- products_retriever = self .__create_cosmic_works_vector_store_retriever ("products" )
106- customers_retriever = self .__create_cosmic_works_vector_store_retriever ("customers" )
107- sales_retriever = self .__create_cosmic_works_vector_store_retriever ("sales" )
108-
109- # create a chain on the retriever to format the documents as JSON
110- products_retriever_chain = products_retriever | format_docs
111- customers_retriever_chain = customers_retriever | format_docs
112- sales_retriever_chain = sales_retriever | format_docs
113103
114- tools = [
115- Tool (
116- name = "vector_search_products" ,
117- func = products_retriever_chain .invoke ,
118- description = """
119- Searches Cosmic Works product information for similar products based
120- on the question. Returns the product information in JSON format.
121- """
122- ),
123- Tool (
124- name = "vector_search_customers" ,
125- func = customers_retriever_chain .invoke ,
126- description = """
127- Searches Cosmic Works customer information and retrieves similar
128- customers based on the question. Returns the customer information
129- in JSON format.
130- """
131- ),
132- Tool (
133- name = "vector_search_sales" ,
134- func = sales_retriever_chain .invoke ,
135- description = """
136- Searches Cosmic Works customer sales information and retrieves sales order
137- details based on the question. Returns the sales order information in JSON format.
138- """
139- ),
140- StructuredTool .from_function (get_product_by_id ),
141- StructuredTool .from_function (get_product_by_sku ),
142- StructuredTool .from_function (get_sales_by_id )
143- ]
144- return tools
104+ # Tools helper methods
105+ def delete_attribute_by_alias (instance : BaseModel , alias :str ):
106+ """
107+ Removes an attribute from a Pydantic model instance by its alias.
108+ """
109+ for model_field in instance .model_fields :
110+ field = instance .model_fields [model_field ]
111+ if field .alias == alias :
112+ delattr (instance , model_field )
113+ return
145114
146- def format_docs (docs :List [Document ]) -> str :
115+ def get_single_item_by_field_name (
116+ container :ContainerProxy ,
117+ field_name :str ,
118+ field_value :str ,
119+ model :Type [T ]) -> T :
147120 """
148- Prepares the product list for the system prompt .
121+ Retrieves a single item from the Azure Cosmos DB NoSQL database by a specific field and value .
149122 """
150- str_docs = []
151- for doc in docs :
152- # Build the product document without the contentVector
153- doc_dict = {"_id" : doc .page_content }
154- doc_dict .update (doc .metadata )
155- if "contentVector" in doc_dict :
156- del doc_dict ["contentVector" ]
157- str_docs .append (json .dumps (doc_dict , default = str ))
158- # Return a single string containing each product JSON representation
159- # separated by two newlines
160- return "\n \n " .join (str_docs )
123+ query = f"SELECT TOP 1 * FROM itm WHERE itm.{ field_name } = @value"
124+ parameters = [
125+ {
126+ "name" : "@value" ,
127+ "value" : field_value
128+ }
129+ ]
130+ item = list (container .query_items (
131+ query = query ,
132+ parameters = parameters ,
133+ enable_cross_partition_query = True
134+ ))[0 ]
135+ item_casted = model (** item )
136+ return item_casted
161137
162138def get_product_by_id (product_id : str ) -> str :
163139 """
164140 Retrieves a product by its ID.
165141 """
166- doc = db .products .find_one ({"_id" : product_id })
167- if "contentVector" in doc :
168- del doc ["contentVector" ]
169- return json .dumps (doc )
142+ item = get_single_item_by_field_name (product_v_container , "id" , product_id , Product )
143+ delete_attribute_by_alias (item , "contentVector" )
144+ return json .dumps (item , indent = 4 , default = str )
170145
171146def get_product_by_sku (sku : str ) -> str :
172147 """
173148 Retrieves a product by its sku.
174149 """
175- doc = db .products .find_one ({"sku" : sku })
176- if "contentVector" in doc :
177- del doc ["contentVector" ]
178- return json .dumps (doc , default = str )
179-
150+ item = get_single_item_by_field_name (product_v_container , "sku" , sku , Product )
151+ delete_attribute_by_alias (item , "contentVector" )
152+ return json .dumps (item , indent = 4 , default = str )
153+
180154def get_sales_by_id (sales_id : str ) -> str :
181155 """
182156 Retrieves a sales order by its ID.
183157 """
184- doc = db .sales .find_one ({"_id" : sales_id })
185- if "contentVector" in doc :
186- del doc ["contentVector" ]
187- return json .dumps (doc , default = str )
188-
158+ item = get_single_item_by_field_name (sales_order_container , "id" , sales_id , SalesOrder )
159+ return json .dumps (item , indent = 4 , default = str )
0 commit comments