Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,54 @@ curl -s --request POST \
--header 'Content-Type: application/json' \
--data @payload.json
```

## Using OCI GenAI hosted models

This agent can also use models hosted by the **OCI Generative AI** service.

### Prerequisites

- OCI CLI configured (or instance/resource principal auth available)
- Access to OCI Generative AI service + a model you can call
- Set your compartment OCID and region

### Example request payload

Provide the model as `oci_genai:<model_ocid>` and pass required OCI parameters in `model_args`.
Tool-calling is disabled in this mode (the agent will still run, but won’t attempt tool calls).

```json
{
"assistant_id": "agent",
"input": {
"messages": [
{
"role": "human",
"content": "Summarize what LangGraph is in 2 sentences."
}
]
},
"context": {
"model": "oci_genai:ocid1.generativeaimodel.oc1..exampleuniqueID",
"enable_tools": false,
"model_args": {
"compartment_id": "ocid1.compartment.oc1..exampleuniqueID",
"region": "us-chicago-1",
"profile": "DEFAULT",
"auth_type": "api_key",
"temperature": 0.2,
"max_tokens": 512
}
},
"stream_mode": "messages-tuple"
}
```

`auth_type` can be one of:
- `api_key` (default; uses your OCI config file)
- `instance_principal`
- `resource_principal`

## License
Copyright (c) 2025 Oracle and/or its affiliates.

Expand Down
Binary file added agent/app/.langgraph_api/.langgraph_ops.pckl
Binary file not shown.
1 change: 1 addition & 0 deletions agent/app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"langchain-tavily>=0.1",
"langchain-ollama>=0.3.10",
"langchain-mcp-adapters>=0.1.11",
"oci>=2.0.0",
]

[project.optional-dependencies]
Expand Down
16 changes: 11 additions & 5 deletions agent/app/src/react_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"""React Agent.
"""React Agent package.

This module defines a custom reasoning and action agent graph.
It invokes tools in a simple loop.
Keep package import side effects minimal so utility modules can be imported in
tests/environments without requiring the full LangGraph runtime dependencies.
"""

from react_agent.graph import graph

__all__ = ["graph"]


def __getattr__(name: str):
if name == "graph":
from react_agent.graph import graph

return graph
raise AttributeError(name)
16 changes: 12 additions & 4 deletions agent/app/src/react_agent/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import os
from dataclasses import dataclass, field, fields
from typing import Annotated, Dict
from typing import Annotated, Any, Dict

from . import prompts

Expand All @@ -22,20 +22,28 @@ class Context:
)

model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
default="anthropic/claude-3-5-sonnet-20240620",
default="anthropic:claude-3-5-sonnet-20240620",
metadata={
"description": "The name of the language model to use for the agent's main interactions. "
"Should be in the form: provider/model-name."
"Should be in the form: provider:model-name."
},
)

model_args: Dict[str, str] = field(default_factory=dict)
model_args: Dict[str, Any] = field(default_factory=dict)

base_url: str = field(
default="http://localhost:11434",
metadata={"description": "URL to a modelserver, like Ollama"},
)

enable_tools: bool = field(
default=True,
metadata={
"description": "Whether the agent should enable tool calling. "
"Some model providers (e.g. OCI GenAI via the OCI SDK) may not support tool calling."
},
)

max_search_results: int = field(
default=10,
metadata={
Expand Down
19 changes: 11 additions & 8 deletions agent/app/src/react_agent/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode
from langgraph.runtime import Runtime
from langchain_mcp_adapters.client import MultiServerMCPClient
from react_agent.context import Context
from react_agent.state import InputState, State
from react_agent.tools import TOOLS
from react_agent.utils import load_chat_model
from langgraph.prebuilt import create_react_agent
from langchain.chat_models import init_chat_model
from langchain_mcp_adapters.client import MultiServerMCPClient
from react_agent.utils import load_chat_model, validate_model_capabilities



# Define the function that calls the model
Expand All @@ -37,14 +34,20 @@ async def call_model(
Returns:
dict: A dictionary containing the model's response message.
"""
# Initialize the model with tool binding. Change the model or add more tools here.
# Initialize the model. Some providers (like OCI GenAI via OCI SDK) may not
# support tool calling; allow disabling tool binding via context.
validate_model_capabilities(
runtime.context.model, enable_tools=runtime.context.enable_tools
)
model = load_chat_model(
runtime.context.model,
base_url=runtime.context.base_url,
model_args=runtime.context.model_args,
).bind_tools(TOOLS)
)
if runtime.context.enable_tools:
model = model.bind_tools(TOOLS)

# model = load_chat_model("ollama/gpt-oss").bind_tools(TOOLS)
# model = load_chat_model("ollama:gpt-oss").bind_tools(TOOLS)
# model = load_chat_model(runtime.context.model).bind_tools(tools)

# Format the system prompt. Customize this to change the agent's behavior.
Expand Down
31 changes: 31 additions & 0 deletions agent/app/src/react_agent/message_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Helpers for working with LangChain message content."""

from __future__ import annotations

from typing import Any

from langchain_core.messages import BaseMessage


def get_message_text(msg: BaseMessage) -> str:
"""Extract readable text from common LangChain message payload shapes."""
return content_to_text(msg.content)


def content_to_text(content: Any) -> str:
"""Flatten common message content block shapes into plain text."""
if isinstance(content, str):
return content
if isinstance(content, dict):
return content.get("text", "")

parts: list[str] = []
for item in content:
if isinstance(item, str):
parts.append(item)
continue
if isinstance(item, dict):
text = item.get("text")
if text:
parts.append(text)
return "".join(parts).strip()
184 changes: 184 additions & 0 deletions agent/app/src/react_agent/oci_genai_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""OCI GenAI ChatModel adapter.

This implements a minimal LangChain-compatible chat model that calls
OCI Generative AI Inference `chat` using the OCI Python SDK.

Notes:
- This adapter is intentionally minimal and only supports *chat*.
- Tool calling is not implemented here. If you enable tools in the graph and
use an OCI model, your model may ignore tools or respond unpredictably.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional

from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import (
AIMessage,
BaseMessage,
HumanMessage,
SystemMessage,
ToolMessage,
)
from langchain_core.outputs import ChatGeneration, ChatResult
from react_agent.message_utils import get_message_text


def _to_genai_message(message: BaseMessage) -> Dict[str, Any]:
"""Convert LangChain messages into OCI GenericChatRequest message dicts."""
content = get_message_text(message)
if isinstance(message, SystemMessage):
role = "SYSTEM"
elif isinstance(message, HumanMessage):
role = "USER"
elif isinstance(message, ToolMessage):
# Generic chat has no explicit "tool" role. Preserve provenance in the
# text payload rather than making tool output look like fresh user input.
role = "USER"
content = f"Tool result:\n{content}"
else:
# AIMessage and everything else
role = "ASSISTANT"

return {
"role": role,
"content": [{"type": "TEXT", "text": content}],
}


class OCIGenAIChatModel(BaseChatModel):
"""LangChain chat model wrapper over OCI Generative AI Inference."""

# These are regular attributes (not pydantic) in current langchain-core.
model_id: str
compartment_id: str
region: str
profile: Optional[str]
auth_type: str
# Optional endpoint override, useful for private endpoints.
endpoint: Optional[str]
# Extra request params
temperature: Optional[float]
max_tokens: Optional[int]
top_p: Optional[float]

def __init__(
self,
*,
model_id: str,
compartment_id: str,
region: str,
profile: Optional[str] = None,
auth_type: str = "api_key",
endpoint: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
top_p: Optional[float] = None,
**kwargs: Any,
):
super().__init__(**kwargs)
self.model_id = model_id
self.compartment_id = compartment_id
self.region = region
self.profile = profile
self.auth_type = auth_type
self.endpoint = endpoint
self.temperature = temperature
self.max_tokens = max_tokens
self.top_p = top_p

@property
def _llm_type(self) -> str:
return "oci_genai"

@property
def _identifying_params(self) -> Dict[str, Any]:
return {
"model_id": self.model_id,
"region": self.region,
"compartment_id": self.compartment_id,
"profile": self.profile,
"auth_type": self.auth_type,
"endpoint": self.endpoint,
}

def _generate(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Any = None,
**kwargs: Any,
) -> ChatResult:
# Import lazily so that users who don't need OCI GenAI don't need the SDK.
import oci
from oci.generative_ai_inference import GenerativeAiInferenceClient
from oci.generative_ai_inference.models import (
ChatDetails,
GenericChatRequest,
OnDemandServingMode,
)

config = oci.config.from_file(profile_name=self.profile) if self.profile else oci.config.from_file()
config["region"] = self.region

if self.auth_type == "instance_principal":
signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
elif self.auth_type == "resource_principal":
signer = oci.auth.signers.get_resource_principals_signer()
else:
signer = None

client_kwargs: Dict[str, Any] = {}
if self.endpoint:
client_kwargs["service_endpoint"] = self.endpoint

client = (
GenerativeAiInferenceClient(config=config, signer=signer, **client_kwargs)
if signer
else GenerativeAiInferenceClient(config=config, **client_kwargs)
)

# Build GenericChatRequest from a python dict to avoid tight coupling
# to SDK constructor signatures.
request: Dict[str, Any] = {
"api_format": "GENERIC",
"messages": [_to_genai_message(m) for m in messages],
"compartment_id": self.compartment_id,
# Some models use/expect these fields; safe to omit when None.
}
if self.temperature is not None:
request["temperature"] = self.temperature
if self.max_tokens is not None:
request["max_tokens"] = self.max_tokens
if self.top_p is not None:
request["top_p"] = self.top_p

# Allow per-call overrides via kwargs
for key in ("temperature", "max_tokens", "top_p"):
if key in kwargs and kwargs[key] is not None:
request[key] = kwargs[key]

chat_details = ChatDetails(
compartment_id=self.compartment_id,
serving_mode=OnDemandServingMode(model_id=self.model_id),
chat_request=GenericChatRequest(**request),
)

resp = client.chat(chat_details)
data = resp.data

# GenericChatResponse typically has: data.chat_response.choices[0].message.content[0].text
text: str = ""
try:
choice0 = data.chat_response.choices[0]
# content is list[ChatContent]
text = "".join(
getattr(c, "text", "") or "" for c in getattr(choice0.message, "content", [])
).strip()
except Exception:
# Fallback: stringify whole response
text = str(data)

ai_msg = AIMessage(content=text)
return ChatResult(generations=[ChatGeneration(message=ai_msg)])
Loading