Skip to content

Commit bc80eab

Browse files
committed
Updated backend API
1 parent 5123741 commit bc80eab

25 files changed

+410
-150
lines changed

Backend/.env.EXAMPLE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
DB_CONNECTION_STRING="AccountEndpoint=https://<cosmos-account-name>.documents.azure.com:443/;AccountKey=<cosmos-account-key>;"
1+
COSMOS_DB_CONNECTION_STRING="AccountEndpoint=https://<cosmos-account-name>.documents.azure.com:443/;AccountKey=<cosmos-account-key>;"
22
AOAI_ENDPOINT = "https://<resource>.openai.azure.com/"
33
AOAI_KEY = "<key>"

Backend/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
venv
1+
.venv
22
__pycache__
33
.env
44

Lines changed: 102 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
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
"""
68
import os
79
import json
8-
from typing import List
9-
import pymongo
10+
from pydantic import BaseModel
11+
from typing import Type, TypeVar, List
1012
from 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"
2229
AOAI_ENDPOINT = os.environ.get("AOAI_ENDPOINT")
2330
AOAI_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

2939
class 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

162138
def 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

171146
def 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+
180154
def 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)

Backend/models/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
This module contains the model definitions of objects
3+
that are present in the Cosmic Works dataset.
4+
"""
5+
from .tag import Tag
6+
from .product import Product, ProductList
7+
from .address import Address
8+
from .password import Password
9+
from .customer import Customer, CustomerList
10+
from .sales_order_detail import SalesOrderDetail
11+
from .sales_order import SalesOrder, SalesOrderList

Backend/models/address.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
Address model
3+
"""
4+
from pydantic import BaseModel, Field
5+
6+
class Address(BaseModel):
7+
"""
8+
The Address class represents the structure of
9+
an address in the Cosmic Works dataset.
10+
"""
11+
address_line_1: str = Field(alias="addressLine1")
12+
address_line_2: str = Field(alias="addressLine2")
13+
city: str
14+
state: str
15+
country: str
16+
zip_code: str = Field(alias="zipCode")

Backend/models/customer.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
Customer and CustomerList models
3+
"""
4+
from datetime import datetime
5+
from typing import List, Optional
6+
from pydantic import BaseModel, Field
7+
from .address import Address
8+
from .password import Password
9+
10+
class Customer(BaseModel):
11+
"""
12+
The Customer class represents a customer in the
13+
Cosmic Works dataset.
14+
15+
The alias feelds are used to map the dataset
16+
field names to the pythonic property names.
17+
"""
18+
id: str = Field(alias="id")
19+
customer_id: str = Field(alias="customerId")
20+
title: Optional[str]
21+
first_name: str = Field(alias="firstName")
22+
last_name: str = Field(alias="lastName")
23+
email_address: str = Field(alias="emailAddress")
24+
phone_number: str = Field(alias="phoneNumber")
25+
creation_date: datetime = Field(alias="creationDate")
26+
addresses: List[Address]
27+
password: Password
28+
sales_order_count: int = Field(alias="salesOrderCount")
29+
30+
class Config:
31+
"""
32+
The Config inner class is used to configure the
33+
behavior of the Pydantic model. In this case,
34+
the Pydantic model will be able to deserialize
35+
data by both the field name and the field alias.
36+
"""
37+
populate_by_name = True
38+
json_encoders = {
39+
datetime: lambda v: v.isoformat()
40+
}
41+
42+
class CustomerList(BaseModel):
43+
"""
44+
The CustomerList class represents a list of customers.
45+
This class is used when deserializing a container/array
46+
of customers.
47+
"""
48+
items: List[Customer]

Backend/models/password.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Password model
3+
"""
4+
from pydantic import BaseModel
5+
6+
class Password(BaseModel):
7+
"""
8+
The Password class represents the structure of
9+
a password in the Cosmic Works dataset.
10+
"""
11+
hash: str
12+
salt: str

0 commit comments

Comments
 (0)