Skip to content

Commit 3dc697c

Browse files
feat(http_llm): add ollama support
1 parent cabae58 commit 3dc697c

File tree

5 files changed

+79
-15
lines changed

5 files changed

+79
-15
lines changed

config.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@
1818
"is_detailed_tool_call": true,
1919
"is_detailed_observation": true
2020
},
21-
"llm": {"temperature": 0.1, "max_tokens": 4096, "top_p": 1},
21+
"llm": {
22+
"cls": "oxygent.llms.OllamaLLM",
23+
"base_url": "http://localhost:11434",
24+
"temperature": 0.1,
25+
"max_tokens": 4096,
26+
"top_p": 1
27+
},
2228
"cache": {"save_dir": "./cache_dir"},
2329
"message": {
2430
"is_send_tool_call": true,

examples/agents/ollama_demo.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""gemma_cli_demo.py
2+
"""
3+
4+
import asyncio
5+
from oxygent import MAS, oxy
6+
from oxygent.utils.env_utils import get_env_var
7+
8+
oxy_space = [
9+
oxy.HttpLLM(
10+
name="local_gemma",
11+
base_url="http://localhost:11434/api/chat",
12+
model_name=get_env_var("DEFAULT_OLLAMA_MODEL"),
13+
llm_params={"temperature": 0.2},
14+
semaphore=1,
15+
timeout=240,
16+
),
17+
18+
oxy.ChatAgent(
19+
name="master_agent",
20+
is_master=True,
21+
llm_model="local_gemma",
22+
),
23+
]
24+
25+
async def chat():
26+
async with MAS(oxy_space=oxy_space) as mas:
27+
history = [{"role": "system", "content": "You are a helpful assistant."}]
28+
29+
while True:
30+
user_in = input("User: ").strip()
31+
if user_in.lower() in {"exit", "quit", "q"}:
32+
break
33+
34+
history.append({"role": "user", "content": user_in})
35+
result = await mas.call(
36+
callee="master_agent",
37+
arguments={"messages": history},
38+
)
39+
assistant_out = result
40+
print(f"Assistant: {assistant_out}\n")
41+
history.append({"role": "assistant", "content": assistant_out})
42+
43+
if __name__ == "__main__":
44+
asyncio.run(chat())

oxygent/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ class Config:
5353
"is_detailed_tool_call": True,
5454
"is_detailed_observation": True,
5555
},
56-
"llm": {"temperature": 0.1, "max_tokens": 4096, "top_p": 1},
56+
"llm": {
57+
"cls": "oxygent.llms.OllamaLLM",
58+
"base_url": "http://localhost:11434",
59+
"temperature": 0.1,
60+
"max_tokens": 4096,
61+
"top_p": 1
62+
},
5763
"cache": {"save_dir": "./cache_dir"},
5864
"message": {
5965
"is_send_tool_call": True,

oxygent/oxy/llms/http_llm.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ...schemas import OxyRequest, OxyResponse, OxyState
1414
from .remote_llm import RemoteLLM
1515

16+
1617
logger = logging.getLogger(__name__)
1718

1819

@@ -22,7 +23,7 @@ class HttpLLM(RemoteLLM):
2223
This class provides a concrete implementation of RemoteLLM for communicating
2324
with remote LLM APIs over HTTP. It handles API authentication, request
2425
formatting, and response parsing for OpenAI-compatible APIs.
25-
"""
26+
"""
2627

2728
async def _execute(self, oxy_request: OxyRequest) -> OxyResponse:
2829
"""Execute an HTTP request to the remote LLM API.
@@ -37,10 +38,11 @@ async def _execute(self, oxy_request: OxyRequest) -> OxyResponse:
3738
Returns:
3839
OxyResponse: The response containing the LLM's output with COMPLETED state.
3940
"""
40-
headers = {
41-
"Content-Type": "application/json",
42-
"Authorization": f"Bearer {self.api_key}",
43-
}
41+
use_openai = self.api_key is not None
42+
url = self.base_url.rstrip("/")
43+
headers = {"Content-Type": "application/json"}
44+
if use_openai:
45+
headers["Authorization"] = f"Bearer {self.api_key}"
4446

4547
# Construct payload for the API request
4648
llm_config = Config.get_llm_config()
@@ -59,16 +61,21 @@ async def _execute(self, oxy_request: OxyRequest) -> OxyResponse:
5961

6062
async with httpx.AsyncClient(timeout=self.timeout) as client:
6163
http_response = await client.post(
62-
self.base_url, headers=headers, json=payload
64+
url, headers=headers, json=payload
6365
)
6466
http_response.raise_for_status()
6567
data = http_response.json()
6668
if "error" in data:
6769
error_message = data["error"].get("message", "Unknown error")
6870
raise ValueError(f"LLM API error: {error_message}")
69-
response_message = data["choices"][0]["message"]
70-
result = response_message.get("content") or response_message.get(
71-
"reasoning_content"
72-
)
71+
72+
if use_openai:
73+
response_message = data["choices"][0]["message"]
74+
result = response_message.get("content") or response_message.get(
75+
"reasoning_content"
76+
)
77+
else: # ollama
78+
result = data["message"]["content"]
79+
7380

7481
return OxyResponse(state=OxyState.COMPLETED, output=result)

oxygent/oxy/llms/remote_llm.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pydantic import Field, field_validator
44

5+
56
from ...schemas import OxyRequest, OxyResponse
67
from .base_llm import BaseLLM
78

@@ -19,11 +20,11 @@ class RemoteLLM(BaseLLM):
1920
model_name: The specific model name to use for requests.
2021
"""
2122

22-
api_key: Optional[str] = Field("")
23+
api_key: Optional[str] = Field(default=None)
2324
base_url: Optional[str] = Field("")
2425
model_name: Optional[str] = Field("")
2526

26-
@field_validator("api_key", "base_url", "model_name")
27+
@field_validator("base_url", "model_name")
2728
@classmethod
2829
def not_empty(cls, value, info):
2930
key = info.field_name
@@ -38,7 +39,7 @@ def not_empty(cls, value, info):
3839
f"Environment variable '{key}' type error: expected str, got {type(value).__name__}."
3940
)
4041

41-
if not value.strip():
42+
if not isinstance(value, str) or not value.strip():
4243
raise ValueError(f"{key} must be a non-empty string")
4344

4445
return value

0 commit comments

Comments
 (0)