One of my more interesting OCI Generative AI experiments was function calling. OCI's generic chat request exposes this directly: GenericChatRequest accepts tools and tool_choice, and an assistant response can contain tool_calls.
This turns a text-only model call into an agent-style loop, while the application—not the LLM—retains execution authority.
1. The Complete Control Flow
User:
"What columns are in SALES_ORDER?"
↓
OCI request contains:
- user message
- tool definitions
- automatic tool selection
↓
Assistant response:
lookup_schema(table_name="SALES_ORDER")
↓
Python validates and executes the function
↓
Python returns a ToolMessage
↓
OCI generates the final explanation
2. Define a Narrow Tool Schema
from oci.generative_ai_inference.models import (
FunctionDefinition,
ToolDefinition,
)
lookup_schema_tool = ToolDefinition(
function=FunctionDefinition(
name="lookup_schema",
description=(
"Return column metadata for one Oracle table. "
"Use this when the user asks about columns "
"or data types."
),
parameters={
"type": "object",
"properties": {
"table_name": {
"type": "string",
"description": "Oracle table name"
}
},
"required": ["table_name"],
"additionalProperties": False,
},
)
)
The tool description is part of routing. Precise names and descriptions make model decisions easier to understand and test.
3. Allow Automatic Tool Selection
from oci.generative_ai_inference.models import (
GenericChatRequest,
ToolChoiceAuto,
UserMessage,
TextContent,
)
request = GenericChatRequest(
messages=[
UserMessage(
content=[
TextContent(
text="What columns are in SALES_ORDER?"
)
]
)
],
tools=[lookup_schema_tool],
tool_choice=ToolChoiceAuto(),
temperature=0.1,
max_tokens=800,
)
The model can answer directly when it does not need a function, or request a tool when live information is required.
4. Send the First Request
details = ChatDetails(
compartment_id=COMPARTMENT_ID,
serving_mode=OnDemandServingMode(
model_id=MODEL_ID
),
chat_request=request,
)
first_response = client.chat(details)
assistant_msg = (
first_response.data.chat_response
.choices[0]
.message
)
5. Inspect the Proposed Call
if not assistant_msg.tool_calls:
answer = assistant_msg.content[0].text
print(answer)
else:
for call in assistant_msg.tool_calls:
print(call.id)
print(call.function.name)
print(call.function.arguments)
This is the key trust boundary: the model proposes a call; Python decides whether it is permitted.
6. Use an Explicit Tool Registry
TOOLS = {
"lookup_schema": lookup_schema,
"run_readonly_query": run_readonly_query,
}
I do not use eval() or dynamically import a model-supplied function name. The name must exist in a controlled registry.
7. Validate Arguments Before Execution
import json
def execute_tool(tool_call):
name = tool_call.function.name
if name not in TOOLS:
raise ValueError(f"Tool not allowed: {name}")
args = json.loads(tool_call.function.arguments)
if name == "lookup_schema":
table_name = args["table_name"].upper()
if not table_name.replace("_", "").isalnum():
raise ValueError("Invalid table name")
args["table_name"] = table_name
return TOOLS[name](**args)
For database functions I would add schema allow-lists, read-only credentials, row limits, SQL validation, execution timeouts, and audit logging.
8. Example Read-Only Oracle Tool
def lookup_schema(table_name):
sql = """
SELECT column_name,
data_type,
nullable
FROM user_tab_columns
WHERE table_name = :table_name
ORDER BY column_id
"""
with connection.cursor() as cur:
cur.execute(sql, table_name=table_name)
return [
{
"column_name": row[0],
"data_type": row[1],
"nullable": row[2],
}
for row in cur
]
The tool returns structured data. It does not need to compose the final English response.
9. Return the Tool Result with ToolMessage
from oci.generative_ai_inference.models import ToolMessage
tool_call = assistant_msg.tool_calls[0]
tool_result = execute_tool(tool_call)
tool_message = ToolMessage(
tool_call_id=tool_call.id,
content=[
TextContent(
text=json.dumps(tool_result)
)
],
)
messages = [
request.messages[0],
assistant_msg,
tool_message,
]
The tool_call_id ties the result to the function request generated by the model.
10. Ask OCI to Continue the Conversation
second_request = GenericChatRequest(
messages=messages,
tools=[lookup_schema_tool],
tool_choice=ToolChoiceAuto(),
temperature=0.1,
max_tokens=800,
)
second_details = ChatDetails(
compartment_id=COMPARTMENT_ID,
serving_mode=OnDemandServingMode(
model_id=MODEL_ID
),
chat_request=second_request,
)
second_response = client.chat(second_details)
final_msg = (
second_response.data.chat_response
.choices[0]
.message
)
print(final_msg.content[0].text)
11. Reasoning and Authority Are Different
Model can decide:
"I need schema information."
Application decides:
"lookup_schema is an approved tool."
"These arguments are valid."
"This credential is read-only."
"This execution is logged."
"Now the call may run."
What I Learned
Function calling is not the same as giving an LLM unrestricted database or operating-system access. A safe implementation uses a narrow tool registry, validated arguments, limited credentials, controlled execution, and structured results returned to the model. OCI's generic chat object model fits this pattern naturally.