Skip to content

Commit 9f5be6a

Browse files
committed
Add example model artifacts.
1 parent 6c36b76 commit 9f5be6a

File tree

7 files changed

+635
-0
lines changed

7 files changed

+635
-0
lines changed

LLM/deployment/model_artifacts/app.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""This module is a bare minimum example of an application.
2+
"""
3+
4+
5+
def invoke(inputs):
6+
return {"message": f"This is an example app. You inputs are: {str(inputs)}"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""This module contains a LangChain agent example to answer question about currency exchange.
2+
3+
It uses OCI Generative AI as the LLM to:
4+
1. Generate arguments for tool calling for obtaining the exchange rate.
5+
2. Process the tool calling results (exchange rates in JSON payload).
6+
3. Answer the user's question.
7+
8+
This module requires the following environment variable:
9+
* PROJECT_COMPARTMENT_OCID, the compartment OCID for access OCI Generative AI service.
10+
11+
By default, OCI model deployment can only access the OCI Generative AI endpoints
12+
within the same region. Custom networking for the model deployment is required
13+
if you deploy the app in a different region.
14+
15+
Custom networking with internet access is required for this app to run the get_exchange_rate() function.
16+
17+
For more information on custom networking, see:
18+
https://docs.oracle.com/en-us/iaas/data-science/using/model-dep-create-cus-net.htm
19+
20+
"""
21+
22+
import os
23+
import requests
24+
from langchain.agents import create_tool_calling_agent, AgentExecutor
25+
from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI
26+
from langchain_core.tools import tool
27+
from langchain_core.prompts import ChatPromptTemplate
28+
29+
30+
# Use LLM from OCI generative AI service
31+
llm = ChatOCIGenAI(
32+
model_id="cohere.command-r-plus",
33+
# Service endpoint is not needed if the generative AI is available in the same region.
34+
# service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
35+
# Make sure you configure custom networking if you use a service endpoint in a different region.
36+
compartment_id=os.environ["PROJECT_COMPARTMENT_OCID"],
37+
model_kwargs={"temperature": 0, "max_tokens": 4000},
38+
auth_type="RESOURCE_PRINCIPAL",
39+
)
40+
41+
42+
@tool
43+
def get_exchange_rate(currency: str) -> str:
44+
"""Obtain the current exchange rates of a currency to other currencies.
45+
46+
Parameters
47+
----------
48+
currency : str
49+
Currency in ISO 4217 3-letter currency code
50+
51+
Returns
52+
-------
53+
dict:
54+
The value of `rates` is a dictionary contains the exchange rates to other currencies,
55+
in which the keys are the ISO 4217 3-letter currency codes.
56+
"""
57+
58+
response = requests.get(f"https://open.er-api.com/v6/latest/{currency}", timeout=10)
59+
return response.json()
60+
61+
62+
tools = [get_exchange_rate]
63+
prompt = ChatPromptTemplate.from_messages(
64+
[
65+
("system", "You are a helpful assistant"),
66+
("placeholder", "{chat_history}"),
67+
("human", "{input}"),
68+
("placeholder", "{agent_scratchpad}"),
69+
]
70+
)
71+
72+
agent = create_tool_calling_agent(llm, tools, prompt)
73+
agent_executor = AgentExecutor(
74+
agent=agent, tools=tools, verbose=True, return_intermediate_steps=False
75+
)
76+
77+
78+
def invoke(message):
79+
return agent_executor.invoke({"input": message})
+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""LangGraph application containing a research node and a chat node
2+
Adapted from https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/
3+
4+
This module requires the following environment variable:
5+
* PROJECT_COMPARTMENT_OCID, the compartment OCID for access OCI Generative AI service.
6+
7+
Custom networking with internet access is required for this app to run Tavily search tool.
8+
9+
For more information on custom networking, see:
10+
https://docs.oracle.com/en-us/iaas/data-science/using/model-dep-create-cus-net.htm
11+
"""
12+
13+
import base64
14+
import os
15+
import operator
16+
import tempfile
17+
import traceback
18+
from typing import Annotated, Sequence
19+
from typing_extensions import TypedDict
20+
21+
from ads.config import COMPARTMENT_OCID
22+
from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI
23+
from langchain_community.tools.tavily_search import TavilySearchResults
24+
from langchain_core.messages import (
25+
AIMessage,
26+
BaseMessage,
27+
HumanMessage,
28+
ToolMessage,
29+
)
30+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
31+
from langchain_core.tools import tool
32+
from langchain_experimental.utilities import PythonREPL
33+
from langgraph.prebuilt import ToolNode
34+
from langgraph.graph import START, END, StateGraph
35+
36+
# Use LLM from OCI generative AI service
37+
llm = ChatOCIGenAI(
38+
model_id="cohere.command-r-plus",
39+
# Service endpoint is not needed if the generative AI is available in the same region.
40+
# service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
41+
compartment_id=COMPARTMENT_OCID,
42+
model_kwargs={"temperature": 0, "max_tokens": 4000},
43+
auth_type="RESOURCE_PRINCIPAL",
44+
)
45+
46+
# Search tool
47+
tavily_tool = TavilySearchResults(max_results=5)
48+
49+
# Python code execution tool
50+
repl = PythonREPL()
51+
52+
53+
@tool
54+
def python_repl(
55+
code: Annotated[str, "The python code to execute to generate your chart."],
56+
):
57+
"""Use this to execute python code. If you want to see the output of a value,
58+
you should print it out with `print(...)`. This is visible to the user."""
59+
try:
60+
# Set the timeout so the code will be run in a separated process
61+
# This will avoid the code changing variables in the current process.
62+
result = repl.run(code, timeout=30)
63+
except BaseException as e:
64+
return f"Failed to execute. Error: {repr(e)}"
65+
result_str = f"Successfully executed:\n```python\n{code}\n```\nStdout: {result}"
66+
return result_str
67+
68+
69+
class AgentState(TypedDict):
70+
"""Represents the state of the agents"""
71+
72+
messages: Annotated[Sequence[BaseMessage], operator.add]
73+
sender: str
74+
75+
76+
class AgentNode:
77+
"""Represents an agent node."""
78+
79+
def __init__(self, name, llm, system_message, tools=None) -> None:
80+
self.name = name
81+
instructions = (
82+
"You are a helpful AI agent,"
83+
" collaborating with other agents work on a task step by step."
84+
" If you are unable to fully finish it, another agent may help where you left off."
85+
" Execute what you can to make progress."
86+
" If you or any of the other assistants have the final answer,"
87+
" or the team cannot make any progress,"
88+
" prefix your response with FINAL ANSWER so the team knows to stop."
89+
)
90+
if tools:
91+
tool_names = ", ".join([tool.name for tool in tools])
92+
instructions += f" You have access to the following tools: {tool_names}.\n"
93+
llm = llm.bind_tools(tools)
94+
instructions += system_message
95+
prompt = ChatPromptTemplate.from_messages(
96+
[
97+
(
98+
"system",
99+
instructions,
100+
),
101+
MessagesPlaceholder(variable_name="messages"),
102+
]
103+
)
104+
prompt = prompt.partial(system_message=system_message)
105+
self.agent = prompt | llm
106+
107+
def __call__(self, state: AgentState) -> dict:
108+
result = self.agent.invoke(state)
109+
# We convert the agent output into a format that is suitable to append to the global state
110+
if not isinstance(result, ToolMessage):
111+
result = AIMessage(**result.dict(exclude={"type", "name"}), name=self.name)
112+
return {
113+
"messages": [result],
114+
# Since we have a strict workflow, we can
115+
# track the sender so we know who to pass to next.
116+
"sender": self.name,
117+
}
118+
119+
120+
# Nodes
121+
RESEARCH_NODE = "research_node"
122+
CHART_NODE = "chart_node"
123+
124+
# research
125+
research_node = AgentNode(
126+
RESEARCH_NODE,
127+
llm,
128+
system_message="You should provide accurate data for plotting the chart.",
129+
tools=[tavily_tool],
130+
)
131+
132+
133+
# temp dir for saving the chart
134+
# Each thread will get a different temp dir
135+
tmp_dir = tempfile.TemporaryDirectory()
136+
print(f"Temp directory: {tmp_dir.name}")
137+
tmp_file = os.path.join(tmp_dir.name, "chart.png")
138+
# chart
139+
chart_node = AgentNode(
140+
CHART_NODE,
141+
llm,
142+
system_message=(
143+
f"Run Python code to plot the chart and save it to a file named {tmp_file}. "
144+
"Response FINAL ANSWER once the chart is plotted successfully."
145+
),
146+
tools=[python_repl],
147+
)
148+
149+
search_tool = ToolNode([tavily_tool])
150+
chart_tool = ToolNode([python_repl])
151+
SEARCH_TOOL = "search_tool"
152+
CHART_TOOL = "chart_tool"
153+
154+
155+
def research_path(state):
156+
"""Router for research_node"""
157+
messages = state["messages"]
158+
last_message = messages[-1]
159+
if "FINAL ANSWER" in last_message.content:
160+
# Any agent decided the work is done
161+
return END
162+
if last_message.tool_calls:
163+
return SEARCH_TOOL
164+
else:
165+
return CHART_NODE
166+
167+
168+
def chart_path(state):
169+
"""Router for chart_node."""
170+
messages = state["messages"]
171+
last_message = messages[-1]
172+
if "FINAL ANSWER" in last_message.content:
173+
# Any agent decided the work is done
174+
return END
175+
if last_message.tool_calls:
176+
return CHART_TOOL
177+
else:
178+
return RESEARCH_NODE
179+
180+
181+
workflow = StateGraph(AgentState)
182+
183+
workflow.add_node(CHART_NODE, chart_node)
184+
workflow.add_node(RESEARCH_NODE, research_node)
185+
186+
workflow.add_node(SEARCH_TOOL, search_tool)
187+
workflow.add_node(CHART_TOOL, chart_tool)
188+
189+
workflow.add_edge(START, RESEARCH_NODE)
190+
191+
workflow.add_conditional_edges(
192+
RESEARCH_NODE, research_path, {n: n for n in [SEARCH_TOOL, CHART_NODE, END]}
193+
)
194+
workflow.add_conditional_edges(
195+
CHART_NODE, chart_path, {n: n for n in [CHART_TOOL, RESEARCH_NODE, END]}
196+
)
197+
198+
199+
workflow.add_edge(CHART_TOOL, CHART_NODE)
200+
workflow.add_edge(SEARCH_TOOL, RESEARCH_NODE)
201+
202+
203+
graph = workflow.compile()
204+
205+
206+
def invoke(message):
207+
"""Invokes the graph."""
208+
events = graph.stream(
209+
{
210+
"messages": [HumanMessage(content=message)],
211+
},
212+
# Maximum number of steps to take in the graph
213+
{"recursion_limit": 10},
214+
)
215+
216+
# Print and save the messages
217+
messages = []
218+
for event in events:
219+
for node, value in event.items():
220+
print(node)
221+
print("-" * 50)
222+
message = value["messages"][-1].content
223+
messages.append(message)
224+
print(message)
225+
print("=" * 50)
226+
227+
# Load the chart and encode it with base64
228+
if os.path.exists(tmp_file):
229+
with open(tmp_file, mode="rb") as f:
230+
chart = base64.b64encode(f.read()).decode()
231+
print(f"Loaded chart from {tmp_file}")
232+
try:
233+
os.remove(tmp_file)
234+
except Exception:
235+
print(f"Failed to remove file {tmp_file}.")
236+
traceback.print_exc()
237+
else:
238+
chart = None
239+
return {"chart": chart, "messages": messages}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""This module contains a invoke() function that runs for more than 1 minute.
2+
3+
To invoke this with model deployment, make sure you specify the "async" parameter
4+
in the payload to save the results into OCI object storage.
5+
"""
6+
7+
import time
8+
9+
10+
def invoke(inputs):
11+
f_time = time.time()
12+
time.sleep(90)
13+
t_time = time.time()
14+
return {
15+
"message": f"This is an example app running for {str(t_time - f_time)} seconds."
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MODEL_ARTIFACT_VERSION: "3.0"
2+
MODEL_DEPLOYMENT:
3+
INFERENCE_CONDA_ENV:
4+
INFERENCE_ENV_PATH: ""
5+
INFERENCE_ENV_SLUG: ""
6+
INFERENCE_ENV_TYPE: ""
7+
INFERENCE_PYTHON_VERSION: ""
8+
MODEL_PROVENANCE:
9+
PROJECT_OCID: ""
10+
TENANCY_OCID: ""
11+
TRAINING_CODE:
12+
ARTIFACT_DIRECTORY: /opt/ds/model/deployed_model
13+
TRAINING_COMPARTMENT_OCID: ""
14+
TRAINING_CONDA_ENV:
15+
TRAINING_ENV_PATH: ""
16+
TRAINING_ENV_SLUG: ""
17+
TRAINING_ENV_TYPE: ""
18+
TRAINING_PYTHON_VERSION: ""
19+
TRAINING_REGION: ""
20+
TRAINING_RESOURCE_OCID: ""
21+
USER_OCID: ""
22+
VM_IMAGE_INTERNAL_ID: ""

0 commit comments

Comments
 (0)