Skip to content

Commit 32307d6

Browse files
Merge pull request #1 from solliancenet/merge-labs
Merge labs and backend repos into 'Labs' and 'Backend' folders
2 parents 975a1d2 + 752d939 commit 32307d6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2930
-0
lines changed

Backend/.env.EXAMPLE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
DB_CONNECTION_STRING="mongodb+srv://<username>:<password>@<host>.mongocluster.cosmos.azure.com/?tls=true&authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000"
2+
AOAI_ENDPOINT = "https://<resource>.openai.azure.com/"
3+
AOAI_KEY = "<key>"

Backend/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
venv
2+
__pycache__
3+
.env
4+
5+
.DS_Store

Backend/DOCKERFILE

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM python:3.11
2+
3+
WORKDIR /code
4+
COPY ./requirements.txt /code/requirements.txt
5+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
6+
COPY . /code
7+
8+
EXPOSE 80
9+
ENV FORWARDED_ALLOW_IPS *
10+
11+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80", "--forwarded-allow-ips", "*", "--proxy-headers"]

Backend/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# cosmos-db-dev-guide-backend-app-python

Backend/api_models/ai_request.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
AIRequest model
3+
"""
4+
from pydantic import BaseModel
5+
6+
class AIRequest(BaseModel):
7+
"""
8+
AIRequest model encapsulates the session_id
9+
and incoming user prompt for the AI agent
10+
to respond to.
11+
"""
12+
session_id: str
13+
prompt: str

Backend/app.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
API entrypoint for backend API.
3+
"""
4+
from fastapi import FastAPI
5+
from fastapi.middleware.cors import CORSMiddleware
6+
7+
from api_models.ai_request import AIRequest
8+
from cosmic_works.cosmic_works_ai_agent import CosmicWorksAIAgent
9+
10+
app = FastAPI()
11+
12+
origins = [
13+
"*"
14+
]
15+
16+
app.add_middleware(
17+
CORSMiddleware,
18+
allow_origins=origins,
19+
allow_credentials=True,
20+
allow_methods=["*"],
21+
allow_headers=["*"],
22+
)
23+
24+
25+
# Agent pool keyed by session_id to retain memories/history in-memory.
26+
# Note: the context is lost every time the service is restarted.
27+
agent_pool = {}
28+
29+
@app.get("/")
30+
def root():
31+
"""
32+
Health probe endpoint.
33+
"""
34+
return {"status": "ready"}
35+
36+
@app.post("/ai")
37+
def run_cosmic_works_ai_agent(request: AIRequest):
38+
"""
39+
Run the Cosmic Works AI agent.
40+
"""
41+
if request.session_id not in agent_pool:
42+
agent_pool[request.session_id] = CosmicWorksAIAgent(request.session_id)
43+
return { "message": agent_pool[request.session_id].run(request.prompt) }
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""
2+
The CosmicWorksAIAgent class encapsulates a LangChain
3+
agent that can be used to answer questions about Cosmic Works
4+
products, customers, and sales.
5+
"""
6+
import os
7+
import json
8+
from typing import List
9+
import pymongo
10+
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
19+
20+
load_dotenv(".env")
21+
DB_CONNECTION_STRING = os.environ.get("DB_CONNECTION_STRING")
22+
AOAI_ENDPOINT = os.environ.get("AOAI_ENDPOINT")
23+
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
28+
29+
class CosmicWorksAIAgent:
30+
"""
31+
The CosmicWorksAIAgent class creates Cosmo, an AI agent
32+
that can be used to answer questions about Cosmic Works
33+
products, customers, and sales.
34+
"""
35+
def __init__(self, session_id: str):
36+
llm = AzureChatOpenAI(
37+
temperature = 0,
38+
openai_api_version = AOAI_API_VERSION,
39+
azure_endpoint = AOAI_ENDPOINT,
40+
openai_api_key = AOAI_KEY,
41+
azure_deployment = COMPLETIONS_DEPLOYMENT
42+
)
43+
self.embedding_model = AzureOpenAIEmbeddings(
44+
openai_api_version = AOAI_API_VERSION,
45+
azure_endpoint = AOAI_ENDPOINT,
46+
openai_api_key = AOAI_KEY,
47+
azure_deployment = EMBEDDINGS_DEPLOYMENT,
48+
chunk_size=10
49+
)
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+
55+
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+
62+
Only answer questions related to Cosmic Works products, customers, and sales orders.
63+
64+
If a question is not related to Cosmic Works products, customers, or sales orders,
65+
respond with "I only answer questions about Cosmic Works"
66+
"""
67+
)
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
74+
)
75+
76+
def run(self, prompt: str) -> str:
77+
"""
78+
Run the AI agent.
79+
"""
80+
result = self.agent_executor({"input": prompt})
81+
return result["output"]
82+
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
113+
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
145+
146+
def format_docs(docs:List[Document]) -> str:
147+
"""
148+
Prepares the product list for the system prompt.
149+
"""
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)
161+
162+
def get_product_by_id(product_id: str) -> str:
163+
"""
164+
Retrieves a product by its ID.
165+
"""
166+
doc = db.products.find_one({"_id": product_id})
167+
if "contentVector" in doc:
168+
del doc["contentVector"]
169+
return json.dumps(doc)
170+
171+
def get_product_by_sku(sku: str) -> str:
172+
"""
173+
Retrieves a product by its sku.
174+
"""
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+
180+
def get_sales_by_id(sales_id: str) -> str:
181+
"""
182+
Retrieves a sales order by its ID.
183+
"""
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+

Backend/requirements.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
pymongo==4.6.1
2+
python-dotenv==1.0.0
3+
requests==2.31.0
4+
pydantic==2.5.2
5+
openai==1.6.0
6+
tenacity==8.2.3
7+
langchain==0.0.352
8+
tiktoken==0.5.2
9+
fastapi==0.108.0
10+
uvicorn==0.25.0
11+
gunicorn==21.2.0

Labs/.env.EXAMPLE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
DB_CONNECTION_STRING="mongodb+srv://<username>:<password>@<host>.mongocluster.cosmos.azure.com/?tls=true&authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000"
2+
AOAI_ENDPOINT = "https://<resource>.openai.azure.com/"
3+
AOAI_KEY = "<key>"

0 commit comments

Comments
 (0)