lightmodel 0.1.4__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lightmodel-0.1.4 → lightmodel-0.3.0}/LICENSE +1 -1
- lightmodel-0.3.0/PKG-INFO +92 -0
- lightmodel-0.3.0/README.md +75 -0
- {lightmodel-0.1.4 → lightmodel-0.3.0}/pyproject.toml +5 -7
- lightmodel-0.3.0/src/lightmodel/__init__.py +12 -0
- lightmodel-0.3.0/src/lightmodel/agent.py +134 -0
- lightmodel-0.3.0/src/lightmodel/exceptions.py +22 -0
- lightmodel-0.3.0/src/lightmodel/model.py +126 -0
- lightmodel-0.3.0/src/lightmodel/tools.py +123 -0
- lightmodel-0.3.0/src/lightmodel.egg-info/PKG-INFO +92 -0
- {lightmodel-0.1.4 → lightmodel-0.3.0}/src/lightmodel.egg-info/SOURCES.txt +2 -0
- {lightmodel-0.1.4 → lightmodel-0.3.0}/src/lightmodel.egg-info/requires.txt +0 -3
- lightmodel-0.1.4/PKG-INFO +0 -126
- lightmodel-0.1.4/README.md +0 -106
- lightmodel-0.1.4/src/lightmodel/__init__.py +0 -6
- lightmodel-0.1.4/src/lightmodel/model.py +0 -126
- lightmodel-0.1.4/src/lightmodel/tools.py +0 -39
- lightmodel-0.1.4/src/lightmodel.egg-info/PKG-INFO +0 -126
- {lightmodel-0.1.4 → lightmodel-0.3.0}/setup.cfg +0 -0
- {lightmodel-0.1.4 → lightmodel-0.3.0}/src/lightmodel.egg-info/dependency_links.txt +0 -0
- {lightmodel-0.1.4 → lightmodel-0.3.0}/src/lightmodel.egg-info/top_level.txt +0 -0
- {lightmodel-0.1.4 → lightmodel-0.3.0}/tests/test_model.py +0 -0
- {lightmodel-0.1.4 → lightmodel-0.3.0}/tests/test_tools.py +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lightmodel
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Ultra-light autonomous AI agents for any OpenAI-compatible API
|
|
5
|
+
Author-email: PlimbAI <plimb.ai.contact@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: llm,ai,agent,openai,nvidia-nim,tool-calling,no-dependencies
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.28
|
|
14
|
+
Provides-Extra: search
|
|
15
|
+
Requires-Dist: beautifulsoup4>=4.12; extra == "search"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# LightModel
|
|
19
|
+
|
|
20
|
+
Ultra-light **autonomous AI agents** for any OpenAI-compatible API.
|
|
21
|
+
No `smolagents`, no `litellm`, no `openai` — just `requests`.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- `LightModel`: HTTP client with automatic retry on transient errors
|
|
26
|
+
- `Agent`: autonomous tool-calling loop with parallel tools and callbacks
|
|
27
|
+
- Built-in tools: `web_search`, `visit_webpage`, `calculator`
|
|
28
|
+
- Extensible: add a tool by subclassing `Tool`
|
|
29
|
+
- Zero heavy dependencies
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
pip install lightmodel
|
|
34
|
+
pip install lightmodel[search] # with beautifulsoup4 for web tools
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
from lightmodel import Agent, LightModel, SimpleDuckDuckGoSearchTool
|
|
39
|
+
|
|
40
|
+
model = LightModel(
|
|
41
|
+
model_id="meta/llama-3.3-70b-instruct",
|
|
42
|
+
api_base="https://integrate.api.nvidia.com/v1",
|
|
43
|
+
api_key="your_key",
|
|
44
|
+
)
|
|
45
|
+
agent = Agent(
|
|
46
|
+
model=model,
|
|
47
|
+
tools=[SimpleDuckDuckGoSearchTool()],
|
|
48
|
+
system_prompt="You are a helpful research assistant.",
|
|
49
|
+
max_steps=6,
|
|
50
|
+
verbose=True,
|
|
51
|
+
)
|
|
52
|
+
print(agent.run("What are the latest AI news in 2026?"))
|
|
53
|
+
|
|
54
|
+
## Multi-turn conversation
|
|
55
|
+
|
|
56
|
+
print(agent.chat("Tell me more about the first result."))
|
|
57
|
+
print(agent.chat("Summarize in one sentence."))
|
|
58
|
+
|
|
59
|
+
## Custom tools
|
|
60
|
+
|
|
61
|
+
from lightmodel import Tool
|
|
62
|
+
|
|
63
|
+
class CalculatorTool(Tool):
|
|
64
|
+
name = "calculator"
|
|
65
|
+
description = "Evaluate arithmetic."
|
|
66
|
+
inputs = {"expression": {"type": "string", "description": "e.g. 2+2"}}
|
|
67
|
+
|
|
68
|
+
def forward(self, expression):
|
|
69
|
+
return str(eval(expression, {"__builtins__": {}}, {}))
|
|
70
|
+
|
|
71
|
+
## Callbacks
|
|
72
|
+
|
|
73
|
+
agent = Agent(
|
|
74
|
+
model=model,
|
|
75
|
+
tools=[SimpleDuckDuckGoSearchTool()],
|
|
76
|
+
callbacks={
|
|
77
|
+
"on_step": lambda step, msg: print(f"step {step}"),
|
|
78
|
+
"on_tool_call": lambda name, args: print(f"→ {name}({args})"),
|
|
79
|
+
"on_tool_result": lambda name, result: print(f"← {result[:80]}"),
|
|
80
|
+
"on_final": lambda answer: print(f"✓ done"),
|
|
81
|
+
"on_error": lambda exc: print(f"✗ {exc}"),
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
## Streaming
|
|
86
|
+
|
|
87
|
+
for chunk in model.stream_chat([{"role": "user", "content": "Hi"}]):
|
|
88
|
+
print(chunk, end="", flush=True)
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# LightModel
|
|
2
|
+
|
|
3
|
+
Ultra-light **autonomous AI agents** for any OpenAI-compatible API.
|
|
4
|
+
No `smolagents`, no `litellm`, no `openai` — just `requests`.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- `LightModel`: HTTP client with automatic retry on transient errors
|
|
9
|
+
- `Agent`: autonomous tool-calling loop with parallel tools and callbacks
|
|
10
|
+
- Built-in tools: `web_search`, `visit_webpage`, `calculator`
|
|
11
|
+
- Extensible: add a tool by subclassing `Tool`
|
|
12
|
+
- Zero heavy dependencies
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
pip install lightmodel
|
|
17
|
+
pip install lightmodel[search] # with beautifulsoup4 for web tools
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
from lightmodel import Agent, LightModel, SimpleDuckDuckGoSearchTool
|
|
22
|
+
|
|
23
|
+
model = LightModel(
|
|
24
|
+
model_id="meta/llama-3.3-70b-instruct",
|
|
25
|
+
api_base="https://integrate.api.nvidia.com/v1",
|
|
26
|
+
api_key="your_key",
|
|
27
|
+
)
|
|
28
|
+
agent = Agent(
|
|
29
|
+
model=model,
|
|
30
|
+
tools=[SimpleDuckDuckGoSearchTool()],
|
|
31
|
+
system_prompt="You are a helpful research assistant.",
|
|
32
|
+
max_steps=6,
|
|
33
|
+
verbose=True,
|
|
34
|
+
)
|
|
35
|
+
print(agent.run("What are the latest AI news in 2026?"))
|
|
36
|
+
|
|
37
|
+
## Multi-turn conversation
|
|
38
|
+
|
|
39
|
+
print(agent.chat("Tell me more about the first result."))
|
|
40
|
+
print(agent.chat("Summarize in one sentence."))
|
|
41
|
+
|
|
42
|
+
## Custom tools
|
|
43
|
+
|
|
44
|
+
from lightmodel import Tool
|
|
45
|
+
|
|
46
|
+
class CalculatorTool(Tool):
|
|
47
|
+
name = "calculator"
|
|
48
|
+
description = "Evaluate arithmetic."
|
|
49
|
+
inputs = {"expression": {"type": "string", "description": "e.g. 2+2"}}
|
|
50
|
+
|
|
51
|
+
def forward(self, expression):
|
|
52
|
+
return str(eval(expression, {"__builtins__": {}}, {}))
|
|
53
|
+
|
|
54
|
+
## Callbacks
|
|
55
|
+
|
|
56
|
+
agent = Agent(
|
|
57
|
+
model=model,
|
|
58
|
+
tools=[SimpleDuckDuckGoSearchTool()],
|
|
59
|
+
callbacks={
|
|
60
|
+
"on_step": lambda step, msg: print(f"step {step}"),
|
|
61
|
+
"on_tool_call": lambda name, args: print(f"→ {name}({args})"),
|
|
62
|
+
"on_tool_result": lambda name, result: print(f"← {result[:80]}"),
|
|
63
|
+
"on_final": lambda answer: print(f"✓ done"),
|
|
64
|
+
"on_error": lambda exc: print(f"✗ {exc}"),
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
## Streaming
|
|
69
|
+
|
|
70
|
+
for chunk in model.stream_chat([{"role": "user", "content": "Hi"}]):
|
|
71
|
+
print(chunk, end="", flush=True)
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -4,22 +4,20 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lightmodel"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Ultra-light
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Ultra-light autonomous AI agents for any OpenAI-compatible API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
|
-
license =
|
|
12
|
-
authors = [{name = "
|
|
13
|
-
keywords = ["
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "PlimbAI", email = "plimb.ai.contact@gmail.com"}]
|
|
13
|
+
keywords = ["llm", "ai", "agent", "openai", "nvidia-nim", "tool-calling", "no-dependencies"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
|
-
"License :: OSI Approved :: MIT License",
|
|
17
16
|
"Operating System :: OS Independent",
|
|
18
17
|
]
|
|
19
18
|
dependencies = ["requests>=2.28"]
|
|
20
19
|
|
|
21
20
|
[project.optional-dependencies]
|
|
22
|
-
smolagents = ["smolagents>=1.0"]
|
|
23
21
|
search = ["beautifulsoup4>=4.12"]
|
|
24
22
|
|
|
25
23
|
[tool.setuptools.packages.find]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""LightModel - Ultra-light autonomous AI agents."""
|
|
2
|
+
from .model import LightModel
|
|
3
|
+
from .tools import Tool, SimpleDuckDuckGoSearchTool, CalculatorTool, VisitWebpageTool
|
|
4
|
+
from .agent import Agent
|
|
5
|
+
from .exceptions import LightModelError, APIError, ToolError, MaxStepsError
|
|
6
|
+
|
|
7
|
+
__version__ = "0.3.0"
|
|
8
|
+
__all__ = [
|
|
9
|
+
"LightModel", "Agent",
|
|
10
|
+
"Tool", "SimpleDuckDuckGoSearchTool", "CalculatorTool", "VisitWebpageTool",
|
|
11
|
+
"LightModelError", "APIError", "ToolError", "MaxStepsError",
|
|
12
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""LightModel Agent - autonomous tool-calling loop."""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
|
|
6
|
+
from .exceptions import MaxStepsError
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Agent:
|
|
12
|
+
"""Autonomous agent using native OpenAI tool calling."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, model, tools=None, system_prompt=None,
|
|
15
|
+
max_steps=10, verbose=False, parallel_tools=True,
|
|
16
|
+
callbacks=None, raise_on_max_steps=False):
|
|
17
|
+
self.model = model
|
|
18
|
+
self.tools = {t.name: t for t in (tools or [])}
|
|
19
|
+
self.system_prompt = system_prompt
|
|
20
|
+
self.max_steps = max_steps
|
|
21
|
+
self.verbose = verbose
|
|
22
|
+
self.parallel_tools = parallel_tools
|
|
23
|
+
self.callbacks = callbacks or {}
|
|
24
|
+
self.raise_on_max_steps = raise_on_max_steps
|
|
25
|
+
self.messages = []
|
|
26
|
+
self.reset()
|
|
27
|
+
|
|
28
|
+
def reset(self):
|
|
29
|
+
"""Clear conversation history (keeps system prompt)."""
|
|
30
|
+
self.messages = []
|
|
31
|
+
if self.system_prompt:
|
|
32
|
+
self.messages.append({"role": "system", "content": self.system_prompt})
|
|
33
|
+
|
|
34
|
+
def _cb(self, name, *args):
|
|
35
|
+
fn = self.callbacks.get(name)
|
|
36
|
+
if fn:
|
|
37
|
+
try:
|
|
38
|
+
fn(*args)
|
|
39
|
+
except Exception:
|
|
40
|
+
logger.exception("Callback %s failed", name)
|
|
41
|
+
|
|
42
|
+
def _log(self, msg):
|
|
43
|
+
if self.verbose:
|
|
44
|
+
print(msg)
|
|
45
|
+
|
|
46
|
+
def run(self, task, reset=True):
|
|
47
|
+
"""Execute a task and return the final text answer."""
|
|
48
|
+
if reset:
|
|
49
|
+
self.reset()
|
|
50
|
+
self.messages.append({"role": "user", "content": task})
|
|
51
|
+
return self._loop()
|
|
52
|
+
|
|
53
|
+
def chat(self, message, reset=False):
|
|
54
|
+
"""Continue the conversation with a new message."""
|
|
55
|
+
if reset:
|
|
56
|
+
self.reset()
|
|
57
|
+
self.messages.append({"role": "user", "content": message})
|
|
58
|
+
return self._loop()
|
|
59
|
+
|
|
60
|
+
def _loop(self):
|
|
61
|
+
openai_tools = [t.to_openai() for t in self.tools.values()] or None
|
|
62
|
+
|
|
63
|
+
for step in range(1, self.max_steps + 1):
|
|
64
|
+
try:
|
|
65
|
+
msg = self.model.chat(self.messages, tools=openai_tools)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self._cb("on_error", e)
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
self._cb("on_step", step, msg)
|
|
71
|
+
|
|
72
|
+
assistant = {"role": "assistant"}
|
|
73
|
+
if msg.get("content"):
|
|
74
|
+
assistant["content"] = msg["content"]
|
|
75
|
+
if msg.get("tool_calls"):
|
|
76
|
+
assistant["tool_calls"] = msg["tool_calls"]
|
|
77
|
+
self.messages.append(assistant)
|
|
78
|
+
|
|
79
|
+
tool_calls = msg.get("tool_calls") or []
|
|
80
|
+
if not tool_calls:
|
|
81
|
+
final = msg.get("content") or ""
|
|
82
|
+
self._cb("on_final", final)
|
|
83
|
+
return final
|
|
84
|
+
|
|
85
|
+
self._log(f"[step {step}] {len(tool_calls)} tool call(s)")
|
|
86
|
+
|
|
87
|
+
if self.parallel_tools and len(tool_calls) > 1:
|
|
88
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
89
|
+
max_workers=len(tool_calls)) as ex:
|
|
90
|
+
results = list(ex.map(self._run_tool, tool_calls))
|
|
91
|
+
else:
|
|
92
|
+
results = [self._run_tool(tc) for tc in tool_calls]
|
|
93
|
+
|
|
94
|
+
for tc, result in zip(tool_calls, results):
|
|
95
|
+
self.messages.append({
|
|
96
|
+
"role": "tool",
|
|
97
|
+
"tool_call_id": tc["id"],
|
|
98
|
+
"content": result,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
err = MaxStepsError(
|
|
102
|
+
f"Reached {self.max_steps} steps without a final answer"
|
|
103
|
+
)
|
|
104
|
+
self._cb("on_error", err)
|
|
105
|
+
if self.raise_on_max_steps:
|
|
106
|
+
raise err
|
|
107
|
+
return str(err)
|
|
108
|
+
|
|
109
|
+
def _run_tool(self, tc):
|
|
110
|
+
name = tc["function"]["name"]
|
|
111
|
+
raw_args = tc["function"]["arguments"]
|
|
112
|
+
try:
|
|
113
|
+
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
|
|
114
|
+
if not isinstance(args, dict):
|
|
115
|
+
args = {}
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
args = {}
|
|
118
|
+
|
|
119
|
+
self._log(f" → {name}({args})")
|
|
120
|
+
self._cb("on_tool_call", name, args)
|
|
121
|
+
|
|
122
|
+
tool = self.tools.get(name)
|
|
123
|
+
if tool is None:
|
|
124
|
+
result = f"Unknown tool: {name}"
|
|
125
|
+
else:
|
|
126
|
+
try:
|
|
127
|
+
result = str(tool.forward(**args))
|
|
128
|
+
except Exception as e:
|
|
129
|
+
result = f"Tool error ({name}): {e}"
|
|
130
|
+
|
|
131
|
+
preview = result[:120].replace("\n", " ")
|
|
132
|
+
self._log(f" ← {preview}...")
|
|
133
|
+
self._cb("on_tool_result", name, result)
|
|
134
|
+
return result
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Custom exceptions for lightmodel."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LightModelError(Exception):
|
|
5
|
+
"""Base exception for all lightmodel errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class APIError(LightModelError):
|
|
9
|
+
"""Raised when the API returns an HTTP error."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, status, message, body=None):
|
|
12
|
+
self.status = status
|
|
13
|
+
self.body = body
|
|
14
|
+
super().__init__(f"API {status}: {message}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolError(LightModelError):
|
|
18
|
+
"""Raised when a tool fails to execute."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MaxStepsError(LightModelError):
|
|
22
|
+
"""Raised when the agent reaches max_steps without a final answer."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""LightModel - pure HTTP client for OpenAI-compatible APIs."""
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from .exceptions import APIError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LightModel:
|
|
13
|
+
"""Client for any OpenAI-compatible /chat/completions API."""
|
|
14
|
+
|
|
15
|
+
RETRIABLE_STATUS = {429, 500, 502, 503, 504}
|
|
16
|
+
|
|
17
|
+
def __init__(self, model_id, api_base, api_key="not-needed",
|
|
18
|
+
temperature=0.7, max_tokens=2048, timeout=120,
|
|
19
|
+
max_retries=3, retry_backoff=2.0, extra=None, headers=None):
|
|
20
|
+
self.model_id = model_id
|
|
21
|
+
base = api_base.rstrip("/")
|
|
22
|
+
if not base.endswith("/chat/completions"):
|
|
23
|
+
base += "/chat/completions"
|
|
24
|
+
self.api_url = base
|
|
25
|
+
self.api_key = api_key
|
|
26
|
+
self.temperature = temperature
|
|
27
|
+
self.max_tokens = max_tokens
|
|
28
|
+
self.timeout = timeout
|
|
29
|
+
self.max_retries = max_retries
|
|
30
|
+
self.retry_backoff = retry_backoff
|
|
31
|
+
self.extra = extra or {}
|
|
32
|
+
self.headers = headers or {}
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f"LightModel({self.model_id!r} @ {self.api_url!r})"
|
|
36
|
+
|
|
37
|
+
def _headers(self):
|
|
38
|
+
h = {"Content-Type": "application/json"}
|
|
39
|
+
if self.api_key:
|
|
40
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
41
|
+
h.update(self.headers)
|
|
42
|
+
return h
|
|
43
|
+
|
|
44
|
+
def _payload(self, messages, tools=None, stream=False, **overrides):
|
|
45
|
+
payload = {
|
|
46
|
+
"model": self.model_id,
|
|
47
|
+
"messages": messages,
|
|
48
|
+
"temperature": overrides.get("temperature", self.temperature),
|
|
49
|
+
"max_tokens": overrides.get("max_tokens", self.max_tokens),
|
|
50
|
+
**self.extra,
|
|
51
|
+
}
|
|
52
|
+
if tools:
|
|
53
|
+
payload["tools"] = tools
|
|
54
|
+
payload["tool_choice"] = overrides.get("tool_choice", "auto")
|
|
55
|
+
if "response_format" in overrides:
|
|
56
|
+
payload["response_format"] = overrides["response_format"]
|
|
57
|
+
if stream:
|
|
58
|
+
payload["stream"] = True
|
|
59
|
+
return payload
|
|
60
|
+
|
|
61
|
+
def chat(self, messages, tools=None, **overrides):
|
|
62
|
+
"""Send messages and return the assistant message dict (with retry)."""
|
|
63
|
+
last_exc = None
|
|
64
|
+
for attempt in range(self.max_retries + 1):
|
|
65
|
+
try:
|
|
66
|
+
return self._do_chat(messages, tools, **overrides)
|
|
67
|
+
except APIError as e:
|
|
68
|
+
if e.status not in self.RETRIABLE_STATUS:
|
|
69
|
+
raise
|
|
70
|
+
last_exc = e
|
|
71
|
+
if attempt < self.max_retries:
|
|
72
|
+
wait = self.retry_backoff ** attempt
|
|
73
|
+
logger.warning("API %s, retrying in %.1fs", e.status, wait)
|
|
74
|
+
time.sleep(wait)
|
|
75
|
+
raise last_exc
|
|
76
|
+
|
|
77
|
+
def _do_chat(self, messages, tools, **overrides):
|
|
78
|
+
r = requests.post(
|
|
79
|
+
self.api_url,
|
|
80
|
+
json=self._payload(messages, tools, **overrides),
|
|
81
|
+
headers=self._headers(),
|
|
82
|
+
timeout=self.timeout,
|
|
83
|
+
)
|
|
84
|
+
if r.status_code >= 400:
|
|
85
|
+
raise APIError(r.status_code, r.text[:300], body=r.text)
|
|
86
|
+
try:
|
|
87
|
+
return r.json()["choices"][0]["message"]
|
|
88
|
+
except Exception as e:
|
|
89
|
+
raise APIError(r.status_code, f"Invalid response: {r.text[:300]}") from e
|
|
90
|
+
|
|
91
|
+
def stream_chat(self, messages, tools=None, **overrides):
|
|
92
|
+
"""Yield content chunks as they arrive (streaming)."""
|
|
93
|
+
r = requests.post(
|
|
94
|
+
self.api_url,
|
|
95
|
+
json=self._payload(messages, tools, stream=True, **overrides),
|
|
96
|
+
headers=self._headers(),
|
|
97
|
+
timeout=self.timeout,
|
|
98
|
+
stream=True,
|
|
99
|
+
)
|
|
100
|
+
if r.status_code >= 400:
|
|
101
|
+
raise APIError(r.status_code, r.text[:300])
|
|
102
|
+
for line in r.iter_lines():
|
|
103
|
+
if not line:
|
|
104
|
+
continue
|
|
105
|
+
line = line.decode("utf-8")
|
|
106
|
+
if not line.startswith("data: "):
|
|
107
|
+
continue
|
|
108
|
+
data = line[6:]
|
|
109
|
+
if data == "[DONE]":
|
|
110
|
+
break
|
|
111
|
+
try:
|
|
112
|
+
chunk = json.loads(data)
|
|
113
|
+
delta = chunk["choices"][0].get("delta", {})
|
|
114
|
+
content = delta.get("content")
|
|
115
|
+
if content:
|
|
116
|
+
yield content
|
|
117
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
def list_models(self):
|
|
121
|
+
"""Return available model IDs (best-effort, server may not support it)."""
|
|
122
|
+
base = self.api_url.rsplit("/chat/completions", 1)[0]
|
|
123
|
+
r = requests.get(f"{base}/models", headers=self._headers(), timeout=30)
|
|
124
|
+
if r.status_code >= 400:
|
|
125
|
+
raise APIError(r.status_code, r.text[:200])
|
|
126
|
+
return [m["id"] for m in r.json().get("data", [])]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Lightweight tools for LightModel agents."""
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _get_bs4():
|
|
6
|
+
try:
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
return BeautifulSoup
|
|
9
|
+
except ImportError:
|
|
10
|
+
raise ImportError(
|
|
11
|
+
"beautifulsoup4 is required: pip install 'lightmodel[search]'"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Tool:
|
|
16
|
+
"""Base class for all tools. Subclass and override forward()."""
|
|
17
|
+
|
|
18
|
+
name = ""
|
|
19
|
+
description = ""
|
|
20
|
+
inputs = {}
|
|
21
|
+
|
|
22
|
+
def forward(self, **kwargs):
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
def to_openai(self):
|
|
26
|
+
props, required = {}, []
|
|
27
|
+
for name, spec in self.inputs.items():
|
|
28
|
+
props[name] = {
|
|
29
|
+
"type": spec.get("type", "string"),
|
|
30
|
+
"description": spec.get("description", ""),
|
|
31
|
+
}
|
|
32
|
+
required.append(name)
|
|
33
|
+
return {
|
|
34
|
+
"type": "function",
|
|
35
|
+
"function": {
|
|
36
|
+
"name": self.name,
|
|
37
|
+
"description": self.description,
|
|
38
|
+
"parameters": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": props,
|
|
41
|
+
"required": required,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
return f"<{self.__class__.__name__} name={self.name!r}>"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SimpleDuckDuckGoSearchTool(Tool):
|
|
51
|
+
"""Lightweight DuckDuckGo web search (HTML scraping, no API key)."""
|
|
52
|
+
|
|
53
|
+
name = "web_search"
|
|
54
|
+
description = "Search the web via DuckDuckGo. Returns the top 5 results."
|
|
55
|
+
inputs = {"query": {"type": "string", "description": "The search query."}}
|
|
56
|
+
|
|
57
|
+
def forward(self, query):
|
|
58
|
+
BeautifulSoup = _get_bs4()
|
|
59
|
+
headers = {"User-Agent": "Mozilla/5.0"}
|
|
60
|
+
try:
|
|
61
|
+
r = requests.post("https://html.duckduckgo.com/html/",
|
|
62
|
+
data={"q": query}, headers=headers, timeout=10)
|
|
63
|
+
r.raise_for_status()
|
|
64
|
+
soup = BeautifulSoup(r.text, "html.parser")
|
|
65
|
+
out = []
|
|
66
|
+
for res in soup.select(".result")[:5]:
|
|
67
|
+
title = res.select_one(".result__a")
|
|
68
|
+
snippet = res.select_one(".result__snippet")
|
|
69
|
+
if title and snippet:
|
|
70
|
+
out.append(
|
|
71
|
+
f"- {title.get_text(strip=True)}\n "
|
|
72
|
+
f"{title['href']}\n {snippet.get_text(strip=True)}"
|
|
73
|
+
)
|
|
74
|
+
return "\n\n".join(out) if out else "No results found."
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return f"Search error: {e}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class VisitWebpageTool(Tool):
|
|
80
|
+
"""Fetch a webpage and return its main text content."""
|
|
81
|
+
|
|
82
|
+
name = "visit_webpage"
|
|
83
|
+
description = "Fetch a webpage and return its main text (truncated)."
|
|
84
|
+
inputs = {"url": {"type": "string", "description": "Full URL to visit"}}
|
|
85
|
+
|
|
86
|
+
def __init__(self, max_chars=3000):
|
|
87
|
+
self.max_chars = max_chars
|
|
88
|
+
|
|
89
|
+
def forward(self, url):
|
|
90
|
+
BeautifulSoup = _get_bs4()
|
|
91
|
+
headers = {"User-Agent": "Mozilla/5.0"}
|
|
92
|
+
try:
|
|
93
|
+
r = requests.get(url, headers=headers, timeout=15)
|
|
94
|
+
r.raise_for_status()
|
|
95
|
+
soup = BeautifulSoup(r.text, "html.parser")
|
|
96
|
+
for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
|
|
97
|
+
tag.decompose()
|
|
98
|
+
text = " ".join(soup.get_text(separator=" ").split())
|
|
99
|
+
return text[:self.max_chars] or "(empty page)"
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return f"Error fetching {url}: {e}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class CalculatorTool(Tool):
|
|
105
|
+
"""Safe arithmetic evaluator."""
|
|
106
|
+
|
|
107
|
+
name = "calculator"
|
|
108
|
+
description = "Evaluate a basic arithmetic expression (numbers, + - * / % ( ) .)."
|
|
109
|
+
inputs = {
|
|
110
|
+
"expression": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "Python arithmetic expression like '2 + 2' or '3 * 7'",
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def forward(self, expression):
|
|
117
|
+
allowed = set("0123456789+-*/().% ")
|
|
118
|
+
if not all(c in allowed for c in expression):
|
|
119
|
+
return "Error: only numbers and + - * / % ( ) . space allowed"
|
|
120
|
+
try:
|
|
121
|
+
return str(eval(expression, {"__builtins__": {}}, {}))
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return f"Error: {e}"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lightmodel
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Ultra-light autonomous AI agents for any OpenAI-compatible API
|
|
5
|
+
Author-email: PlimbAI <plimb.ai.contact@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: llm,ai,agent,openai,nvidia-nim,tool-calling,no-dependencies
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.28
|
|
14
|
+
Provides-Extra: search
|
|
15
|
+
Requires-Dist: beautifulsoup4>=4.12; extra == "search"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# LightModel
|
|
19
|
+
|
|
20
|
+
Ultra-light **autonomous AI agents** for any OpenAI-compatible API.
|
|
21
|
+
No `smolagents`, no `litellm`, no `openai` — just `requests`.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- `LightModel`: HTTP client with automatic retry on transient errors
|
|
26
|
+
- `Agent`: autonomous tool-calling loop with parallel tools and callbacks
|
|
27
|
+
- Built-in tools: `web_search`, `visit_webpage`, `calculator`
|
|
28
|
+
- Extensible: add a tool by subclassing `Tool`
|
|
29
|
+
- Zero heavy dependencies
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
pip install lightmodel
|
|
34
|
+
pip install lightmodel[search] # with beautifulsoup4 for web tools
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
from lightmodel import Agent, LightModel, SimpleDuckDuckGoSearchTool
|
|
39
|
+
|
|
40
|
+
model = LightModel(
|
|
41
|
+
model_id="meta/llama-3.3-70b-instruct",
|
|
42
|
+
api_base="https://integrate.api.nvidia.com/v1",
|
|
43
|
+
api_key="your_key",
|
|
44
|
+
)
|
|
45
|
+
agent = Agent(
|
|
46
|
+
model=model,
|
|
47
|
+
tools=[SimpleDuckDuckGoSearchTool()],
|
|
48
|
+
system_prompt="You are a helpful research assistant.",
|
|
49
|
+
max_steps=6,
|
|
50
|
+
verbose=True,
|
|
51
|
+
)
|
|
52
|
+
print(agent.run("What are the latest AI news in 2026?"))
|
|
53
|
+
|
|
54
|
+
## Multi-turn conversation
|
|
55
|
+
|
|
56
|
+
print(agent.chat("Tell me more about the first result."))
|
|
57
|
+
print(agent.chat("Summarize in one sentence."))
|
|
58
|
+
|
|
59
|
+
## Custom tools
|
|
60
|
+
|
|
61
|
+
from lightmodel import Tool
|
|
62
|
+
|
|
63
|
+
class CalculatorTool(Tool):
|
|
64
|
+
name = "calculator"
|
|
65
|
+
description = "Evaluate arithmetic."
|
|
66
|
+
inputs = {"expression": {"type": "string", "description": "e.g. 2+2"}}
|
|
67
|
+
|
|
68
|
+
def forward(self, expression):
|
|
69
|
+
return str(eval(expression, {"__builtins__": {}}, {}))
|
|
70
|
+
|
|
71
|
+
## Callbacks
|
|
72
|
+
|
|
73
|
+
agent = Agent(
|
|
74
|
+
model=model,
|
|
75
|
+
tools=[SimpleDuckDuckGoSearchTool()],
|
|
76
|
+
callbacks={
|
|
77
|
+
"on_step": lambda step, msg: print(f"step {step}"),
|
|
78
|
+
"on_tool_call": lambda name, args: print(f"→ {name}({args})"),
|
|
79
|
+
"on_tool_result": lambda name, result: print(f"← {result[:80]}"),
|
|
80
|
+
"on_final": lambda answer: print(f"✓ done"),
|
|
81
|
+
"on_error": lambda exc: print(f"✗ {exc}"),
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
## Streaming
|
|
86
|
+
|
|
87
|
+
for chunk in model.stream_chat([{"role": "user", "content": "Hi"}]):
|
|
88
|
+
print(chunk, end="", flush=True)
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
lightmodel-0.1.4/PKG-INFO
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lightmodel
|
|
3
|
-
Version: 0.1.4
|
|
4
|
-
Summary: Ultra-light Model adapter and web search tool for smolagents - works with any OpenAI-compatible API
|
|
5
|
-
Author-email: PlimbAi <plimb.ai.contact@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Keywords: smolagents,llm,ai,agent,openai,nvidia-nim,duckduckgo,web-search
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: requests>=2.28
|
|
15
|
-
Provides-Extra: smolagents
|
|
16
|
-
Requires-Dist: smolagents>=1.0; extra == "smolagents"
|
|
17
|
-
Provides-Extra: search
|
|
18
|
-
Requires-Dist: beautifulsoup4>=4.12; extra == "search"
|
|
19
|
-
Dynamic: license-file
|
|
20
|
-
|
|
21
|
-
# LightModel
|
|
22
|
-
|
|
23
|
-
Ultra-light **Model adapter** and **web search tool** for [smolagents](https://github.com/huggingface/smolagents).
|
|
24
|
-
|
|
25
|
-
Works with **any** OpenAI-compatible API: NVIDIA NIM, Ollama, vLLM, LM Studio,
|
|
26
|
-
Groq, OpenRouter, Together, DeepSeek, or any custom URL. No heavy dependencies,
|
|
27
|
-
no Rust compilation, runs everywhere (including Termux on Android).
|
|
28
|
-
|
|
29
|
-
## Features
|
|
30
|
-
|
|
31
|
-
- `LightModel`: a drop-in `Model` adapter for smolagents using only `requests`.
|
|
32
|
-
- `SimpleDuckDuckGoSearchTool`: a lightweight web search tool that scrapes
|
|
33
|
-
DuckDuckGo HTML and does not require `ddgs`, `primp`, or Rust toolchain.
|
|
34
|
-
- Zero heavy dependencies: only `requests` (and `beautifulsoup4` for search).
|
|
35
|
-
- Works with any endpoint exposing an OpenAI-compatible `/chat/completions` API.
|
|
36
|
-
|
|
37
|
-
## Installation
|
|
38
|
-
|
|
39
|
-
pip install lightmodel
|
|
40
|
-
pip install lightmodel[smolagents] # with smolagents
|
|
41
|
-
pip install lightmodel[search] # with beautifulsoup4 for web search
|
|
42
|
-
|
|
43
|
-
On Termux or restricted environments, install `smolagents` without its heavy
|
|
44
|
-
dependencies:
|
|
45
|
-
|
|
46
|
-
export HF_HUB_DISABLE_XET=1
|
|
47
|
-
pip install --no-deps smolagents
|
|
48
|
-
pip install --no-deps "huggingface-hub==0.31.2"
|
|
49
|
-
pip install rich jinja2 pillow python-dotenv requests beautifulsoup4
|
|
50
|
-
pip install lightmodel
|
|
51
|
-
|
|
52
|
-
## Usage
|
|
53
|
-
|
|
54
|
-
### Model only
|
|
55
|
-
|
|
56
|
-
from smolagents import CodeAgent
|
|
57
|
-
from lightmodel import LightModel
|
|
58
|
-
|
|
59
|
-
model = LightModel(
|
|
60
|
-
model_id="meta/llama3-70b-instruct",
|
|
61
|
-
api_base="https://integrate.api.nvidia.com/v1",
|
|
62
|
-
api_key="your_nvidia_key",
|
|
63
|
-
)
|
|
64
|
-
agent = CodeAgent(tools=[], model=model)
|
|
65
|
-
agent.run("Explain quantum entanglement in one sentence.")
|
|
66
|
-
|
|
67
|
-
### Model + web search
|
|
68
|
-
|
|
69
|
-
from smolagents import CodeAgent
|
|
70
|
-
from lightmodel import LightModel, SimpleDuckDuckGoSearchTool
|
|
71
|
-
|
|
72
|
-
model = LightModel(
|
|
73
|
-
model_id="meta/llama3-70b-instruct",
|
|
74
|
-
api_base="https://integrate.api.nvidia.com/v1",
|
|
75
|
-
api_key="your_nvidia_key",
|
|
76
|
-
)
|
|
77
|
-
agent = CodeAgent(
|
|
78
|
-
tools=[SimpleDuckDuckGoSearchTool()],
|
|
79
|
-
model=model,
|
|
80
|
-
)
|
|
81
|
-
agent.run("What are the latest AI news in 2026?")
|
|
82
|
-
|
|
83
|
-
### With Ollama (local)
|
|
84
|
-
|
|
85
|
-
model = LightModel(
|
|
86
|
-
model_id="llama3",
|
|
87
|
-
api_base="http://localhost:11434/v1",
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
### With any custom URL
|
|
91
|
-
|
|
92
|
-
model = LightModel(
|
|
93
|
-
model_id="my-model",
|
|
94
|
-
api_base="https://my-server.example.com/v1",
|
|
95
|
-
api_key="my-key",
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
## API reference
|
|
99
|
-
|
|
100
|
-
### `LightModel`
|
|
101
|
-
|
|
102
|
-
LightModel(
|
|
103
|
-
model_id: str,
|
|
104
|
-
api_base: str,
|
|
105
|
-
api_key: str = "not-needed",
|
|
106
|
-
temperature: float = 0.7,
|
|
107
|
-
max_tokens: int = 2048,
|
|
108
|
-
timeout: int = 120,
|
|
109
|
-
extra: dict | None = None,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
- `model_id`: model name sent to the API.
|
|
113
|
-
- `api_base`: base URL. `/chat/completions` is appended automatically.
|
|
114
|
-
- `api_key`: bearer token (optional for local servers).
|
|
115
|
-
- `extra`: extra JSON fields merged into the request payload.
|
|
116
|
-
|
|
117
|
-
### `SimpleDuckDuckGoSearchTool`
|
|
118
|
-
|
|
119
|
-
SimpleDuckDuckGoSearchTool()
|
|
120
|
-
|
|
121
|
-
Scrapes DuckDuckGo HTML results and returns the top 5 as formatted text.
|
|
122
|
-
No API key required. No Rust dependency.
|
|
123
|
-
|
|
124
|
-
## License
|
|
125
|
-
|
|
126
|
-
MIT
|
lightmodel-0.1.4/README.md
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# LightModel
|
|
2
|
-
|
|
3
|
-
Ultra-light **Model adapter** and **web search tool** for [smolagents](https://github.com/huggingface/smolagents).
|
|
4
|
-
|
|
5
|
-
Works with **any** OpenAI-compatible API: NVIDIA NIM, Ollama, vLLM, LM Studio,
|
|
6
|
-
Groq, OpenRouter, Together, DeepSeek, or any custom URL. No heavy dependencies,
|
|
7
|
-
no Rust compilation, runs everywhere (including Termux on Android).
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
|
|
11
|
-
- `LightModel`: a drop-in `Model` adapter for smolagents using only `requests`.
|
|
12
|
-
- `SimpleDuckDuckGoSearchTool`: a lightweight web search tool that scrapes
|
|
13
|
-
DuckDuckGo HTML and does not require `ddgs`, `primp`, or Rust toolchain.
|
|
14
|
-
- Zero heavy dependencies: only `requests` (and `beautifulsoup4` for search).
|
|
15
|
-
- Works with any endpoint exposing an OpenAI-compatible `/chat/completions` API.
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
18
|
-
|
|
19
|
-
pip install lightmodel
|
|
20
|
-
pip install lightmodel[smolagents] # with smolagents
|
|
21
|
-
pip install lightmodel[search] # with beautifulsoup4 for web search
|
|
22
|
-
|
|
23
|
-
On Termux or restricted environments, install `smolagents` without its heavy
|
|
24
|
-
dependencies:
|
|
25
|
-
|
|
26
|
-
export HF_HUB_DISABLE_XET=1
|
|
27
|
-
pip install --no-deps smolagents
|
|
28
|
-
pip install --no-deps "huggingface-hub==0.31.2"
|
|
29
|
-
pip install rich jinja2 pillow python-dotenv requests beautifulsoup4
|
|
30
|
-
pip install lightmodel
|
|
31
|
-
|
|
32
|
-
## Usage
|
|
33
|
-
|
|
34
|
-
### Model only
|
|
35
|
-
|
|
36
|
-
from smolagents import CodeAgent
|
|
37
|
-
from lightmodel import LightModel
|
|
38
|
-
|
|
39
|
-
model = LightModel(
|
|
40
|
-
model_id="meta/llama3-70b-instruct",
|
|
41
|
-
api_base="https://integrate.api.nvidia.com/v1",
|
|
42
|
-
api_key="your_nvidia_key",
|
|
43
|
-
)
|
|
44
|
-
agent = CodeAgent(tools=[], model=model)
|
|
45
|
-
agent.run("Explain quantum entanglement in one sentence.")
|
|
46
|
-
|
|
47
|
-
### Model + web search
|
|
48
|
-
|
|
49
|
-
from smolagents import CodeAgent
|
|
50
|
-
from lightmodel import LightModel, SimpleDuckDuckGoSearchTool
|
|
51
|
-
|
|
52
|
-
model = LightModel(
|
|
53
|
-
model_id="meta/llama3-70b-instruct",
|
|
54
|
-
api_base="https://integrate.api.nvidia.com/v1",
|
|
55
|
-
api_key="your_nvidia_key",
|
|
56
|
-
)
|
|
57
|
-
agent = CodeAgent(
|
|
58
|
-
tools=[SimpleDuckDuckGoSearchTool()],
|
|
59
|
-
model=model,
|
|
60
|
-
)
|
|
61
|
-
agent.run("What are the latest AI news in 2026?")
|
|
62
|
-
|
|
63
|
-
### With Ollama (local)
|
|
64
|
-
|
|
65
|
-
model = LightModel(
|
|
66
|
-
model_id="llama3",
|
|
67
|
-
api_base="http://localhost:11434/v1",
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
### With any custom URL
|
|
71
|
-
|
|
72
|
-
model = LightModel(
|
|
73
|
-
model_id="my-model",
|
|
74
|
-
api_base="https://my-server.example.com/v1",
|
|
75
|
-
api_key="my-key",
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
## API reference
|
|
79
|
-
|
|
80
|
-
### `LightModel`
|
|
81
|
-
|
|
82
|
-
LightModel(
|
|
83
|
-
model_id: str,
|
|
84
|
-
api_base: str,
|
|
85
|
-
api_key: str = "not-needed",
|
|
86
|
-
temperature: float = 0.7,
|
|
87
|
-
max_tokens: int = 2048,
|
|
88
|
-
timeout: int = 120,
|
|
89
|
-
extra: dict | None = None,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
- `model_id`: model name sent to the API.
|
|
93
|
-
- `api_base`: base URL. `/chat/completions` is appended automatically.
|
|
94
|
-
- `api_key`: bearer token (optional for local servers).
|
|
95
|
-
- `extra`: extra JSON fields merged into the request payload.
|
|
96
|
-
|
|
97
|
-
### `SimpleDuckDuckGoSearchTool`
|
|
98
|
-
|
|
99
|
-
SimpleDuckDuckGoSearchTool()
|
|
100
|
-
|
|
101
|
-
Scrapes DuckDuckGo HTML results and returns the top 5 as formatted text.
|
|
102
|
-
No API key required. No Rust dependency.
|
|
103
|
-
|
|
104
|
-
## License
|
|
105
|
-
|
|
106
|
-
MIT
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"""LightModel - lightweight Model adapter for smolagents."""
|
|
2
|
-
import json
|
|
3
|
-
import requests
|
|
4
|
-
|
|
5
|
-
from smolagents import Model
|
|
6
|
-
|
|
7
|
-
try:
|
|
8
|
-
from smolagents.models import ChatMessage, MessageRole
|
|
9
|
-
ROLE_ASSISTANT = MessageRole.ASSISTANT
|
|
10
|
-
except ImportError:
|
|
11
|
-
from smolagents.models import ChatMessage
|
|
12
|
-
ROLE_ASSISTANT = "assistant"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class LightModel(Model):
|
|
16
|
-
"""Universal adapter for any OpenAI-compatible API with tool calling."""
|
|
17
|
-
|
|
18
|
-
def __init__(self, model_id, api_base, api_key="not-needed",
|
|
19
|
-
temperature=0.7, max_tokens=2048, timeout=120, extra=None):
|
|
20
|
-
super().__init__()
|
|
21
|
-
self.model_id = model_id
|
|
22
|
-
base = api_base.rstrip("/")
|
|
23
|
-
if not base.endswith("/chat/completions"):
|
|
24
|
-
base += "/chat/completions"
|
|
25
|
-
self.api_url = base
|
|
26
|
-
self.api_key = api_key
|
|
27
|
-
self.temperature = temperature
|
|
28
|
-
self.max_tokens = max_tokens
|
|
29
|
-
self.timeout = timeout
|
|
30
|
-
self.extra = extra or {}
|
|
31
|
-
|
|
32
|
-
def generate(self, messages, stop_sequences=None, response_format=None,
|
|
33
|
-
tools_to_call_from=None, **kwargs):
|
|
34
|
-
headers = {"Content-Type": "application/json"}
|
|
35
|
-
if self.api_key:
|
|
36
|
-
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
37
|
-
|
|
38
|
-
payload = {
|
|
39
|
-
"model": self.model_id,
|
|
40
|
-
"messages": self._to_openai(messages),
|
|
41
|
-
"temperature": self.temperature,
|
|
42
|
-
"max_tokens": self.max_tokens,
|
|
43
|
-
**self.extra,
|
|
44
|
-
}
|
|
45
|
-
if tools_to_call_from:
|
|
46
|
-
payload["tools"] = self._tools_to_openai(tools_to_call_from)
|
|
47
|
-
payload["tool_choice"] = "auto"
|
|
48
|
-
if stop_sequences:
|
|
49
|
-
payload["stop"] = stop_sequences
|
|
50
|
-
|
|
51
|
-
r = requests.post(self.api_url, json=payload, headers=headers,
|
|
52
|
-
timeout=self.timeout)
|
|
53
|
-
if r.status_code >= 400:
|
|
54
|
-
raise RuntimeError(f"API {r.status_code}: {r.text[:300]}")
|
|
55
|
-
|
|
56
|
-
try:
|
|
57
|
-
msg = r.json()["choices"][0]["message"]
|
|
58
|
-
except (KeyError, IndexError):
|
|
59
|
-
raise RuntimeError(f"Invalid response: {r.text[:300]}")
|
|
60
|
-
|
|
61
|
-
content = msg.get("content") or ""
|
|
62
|
-
raw_calls = msg.get("tool_calls") or []
|
|
63
|
-
chat_msg = ChatMessage(role=ROLE_ASSISTANT, content=content)
|
|
64
|
-
if raw_calls:
|
|
65
|
-
chat_msg.tool_calls = [
|
|
66
|
-
{
|
|
67
|
-
"id": tc.get("id", f"call_{i}"),
|
|
68
|
-
"type": "function",
|
|
69
|
-
"function": {
|
|
70
|
-
"name": tc["function"]["name"],
|
|
71
|
-
"arguments": tc["function"]["arguments"],
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
for i, tc in enumerate(raw_calls)
|
|
75
|
-
]
|
|
76
|
-
return chat_msg
|
|
77
|
-
|
|
78
|
-
@staticmethod
|
|
79
|
-
def _tools_to_openai(tools):
|
|
80
|
-
out = []
|
|
81
|
-
for t in tools:
|
|
82
|
-
props, required = {}, []
|
|
83
|
-
for name, spec in (t.inputs or {}).items():
|
|
84
|
-
props[name] = {
|
|
85
|
-
"type": spec.get("type", "string"),
|
|
86
|
-
"description": spec.get("description", ""),
|
|
87
|
-
}
|
|
88
|
-
required.append(name)
|
|
89
|
-
out.append({
|
|
90
|
-
"type": "function",
|
|
91
|
-
"function": {
|
|
92
|
-
"name": t.name,
|
|
93
|
-
"description": t.description,
|
|
94
|
-
"parameters": {
|
|
95
|
-
"type": "object",
|
|
96
|
-
"properties": props,
|
|
97
|
-
"required": required,
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
return out
|
|
102
|
-
|
|
103
|
-
@staticmethod
|
|
104
|
-
def _to_openai(messages):
|
|
105
|
-
ROLE_MAP = {"tool-call": "assistant"}
|
|
106
|
-
out = []
|
|
107
|
-
for m in messages:
|
|
108
|
-
if isinstance(m, dict):
|
|
109
|
-
role, content = m.get("role", "user"), m.get("content", "")
|
|
110
|
-
tool_call_id = m.get("tool_call_id")
|
|
111
|
-
else:
|
|
112
|
-
role = getattr(m, "role", "user")
|
|
113
|
-
content = getattr(m, "content", "") or ""
|
|
114
|
-
tool_call_id = getattr(m, "tool_call_id", None)
|
|
115
|
-
if hasattr(role, "value"):
|
|
116
|
-
role = role.value
|
|
117
|
-
role = str(role)
|
|
118
|
-
if role in ("tool-response", "tool"):
|
|
119
|
-
msg = {"role": "tool", "content": str(content)}
|
|
120
|
-
if tool_call_id:
|
|
121
|
-
msg["tool_call_id"] = tool_call_id
|
|
122
|
-
out.append(msg)
|
|
123
|
-
continue
|
|
124
|
-
role = ROLE_MAP.get(role, role)
|
|
125
|
-
out.append({"role": role, "content": str(content)})
|
|
126
|
-
return out
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Lightweight tools for smolagents."""
|
|
2
|
-
import requests
|
|
3
|
-
from bs4 import BeautifulSoup
|
|
4
|
-
from smolagents import Tool
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class SimpleDuckDuckGoSearchTool(Tool):
|
|
8
|
-
"""Lightweight DuckDuckGo web search tool (HTML scraping, no Rust)."""
|
|
9
|
-
|
|
10
|
-
name = "web_search"
|
|
11
|
-
description = "Search the web for information via DuckDuckGo."
|
|
12
|
-
inputs = {
|
|
13
|
-
"query": {
|
|
14
|
-
"type": "string",
|
|
15
|
-
"description": "The search query.",
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
output_type = "string"
|
|
19
|
-
|
|
20
|
-
def forward(self, query: str) -> str:
|
|
21
|
-
url = "https://html.duckduckgo.com/html/"
|
|
22
|
-
headers = {"User-Agent": "Mozilla/5.0"}
|
|
23
|
-
params = {"q": query}
|
|
24
|
-
try:
|
|
25
|
-
response = requests.post(url, data=params, headers=headers, timeout=10)
|
|
26
|
-
response.raise_for_status()
|
|
27
|
-
soup = BeautifulSoup(response.text, "html.parser")
|
|
28
|
-
results = []
|
|
29
|
-
for result in soup.select(".result")[:5]:
|
|
30
|
-
title_tag = result.select_one(".result__a")
|
|
31
|
-
snippet_tag = result.select_one(".result__snippet")
|
|
32
|
-
if title_tag and snippet_tag:
|
|
33
|
-
title = title_tag.get_text(strip=True)
|
|
34
|
-
link = title_tag["href"]
|
|
35
|
-
snippet = snippet_tag.get_text(strip=True)
|
|
36
|
-
results.append(f"- {title}\n {link}\n {snippet}")
|
|
37
|
-
return "\n\n".join(results) if results else "No results found."
|
|
38
|
-
except Exception as e:
|
|
39
|
-
return f"Search error: {e}"
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lightmodel
|
|
3
|
-
Version: 0.1.4
|
|
4
|
-
Summary: Ultra-light Model adapter and web search tool for smolagents - works with any OpenAI-compatible API
|
|
5
|
-
Author-email: PlimbAi <plimb.ai.contact@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Keywords: smolagents,llm,ai,agent,openai,nvidia-nim,duckduckgo,web-search
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: requests>=2.28
|
|
15
|
-
Provides-Extra: smolagents
|
|
16
|
-
Requires-Dist: smolagents>=1.0; extra == "smolagents"
|
|
17
|
-
Provides-Extra: search
|
|
18
|
-
Requires-Dist: beautifulsoup4>=4.12; extra == "search"
|
|
19
|
-
Dynamic: license-file
|
|
20
|
-
|
|
21
|
-
# LightModel
|
|
22
|
-
|
|
23
|
-
Ultra-light **Model adapter** and **web search tool** for [smolagents](https://github.com/huggingface/smolagents).
|
|
24
|
-
|
|
25
|
-
Works with **any** OpenAI-compatible API: NVIDIA NIM, Ollama, vLLM, LM Studio,
|
|
26
|
-
Groq, OpenRouter, Together, DeepSeek, or any custom URL. No heavy dependencies,
|
|
27
|
-
no Rust compilation, runs everywhere (including Termux on Android).
|
|
28
|
-
|
|
29
|
-
## Features
|
|
30
|
-
|
|
31
|
-
- `LightModel`: a drop-in `Model` adapter for smolagents using only `requests`.
|
|
32
|
-
- `SimpleDuckDuckGoSearchTool`: a lightweight web search tool that scrapes
|
|
33
|
-
DuckDuckGo HTML and does not require `ddgs`, `primp`, or Rust toolchain.
|
|
34
|
-
- Zero heavy dependencies: only `requests` (and `beautifulsoup4` for search).
|
|
35
|
-
- Works with any endpoint exposing an OpenAI-compatible `/chat/completions` API.
|
|
36
|
-
|
|
37
|
-
## Installation
|
|
38
|
-
|
|
39
|
-
pip install lightmodel
|
|
40
|
-
pip install lightmodel[smolagents] # with smolagents
|
|
41
|
-
pip install lightmodel[search] # with beautifulsoup4 for web search
|
|
42
|
-
|
|
43
|
-
On Termux or restricted environments, install `smolagents` without its heavy
|
|
44
|
-
dependencies:
|
|
45
|
-
|
|
46
|
-
export HF_HUB_DISABLE_XET=1
|
|
47
|
-
pip install --no-deps smolagents
|
|
48
|
-
pip install --no-deps "huggingface-hub==0.31.2"
|
|
49
|
-
pip install rich jinja2 pillow python-dotenv requests beautifulsoup4
|
|
50
|
-
pip install lightmodel
|
|
51
|
-
|
|
52
|
-
## Usage
|
|
53
|
-
|
|
54
|
-
### Model only
|
|
55
|
-
|
|
56
|
-
from smolagents import CodeAgent
|
|
57
|
-
from lightmodel import LightModel
|
|
58
|
-
|
|
59
|
-
model = LightModel(
|
|
60
|
-
model_id="meta/llama3-70b-instruct",
|
|
61
|
-
api_base="https://integrate.api.nvidia.com/v1",
|
|
62
|
-
api_key="your_nvidia_key",
|
|
63
|
-
)
|
|
64
|
-
agent = CodeAgent(tools=[], model=model)
|
|
65
|
-
agent.run("Explain quantum entanglement in one sentence.")
|
|
66
|
-
|
|
67
|
-
### Model + web search
|
|
68
|
-
|
|
69
|
-
from smolagents import CodeAgent
|
|
70
|
-
from lightmodel import LightModel, SimpleDuckDuckGoSearchTool
|
|
71
|
-
|
|
72
|
-
model = LightModel(
|
|
73
|
-
model_id="meta/llama3-70b-instruct",
|
|
74
|
-
api_base="https://integrate.api.nvidia.com/v1",
|
|
75
|
-
api_key="your_nvidia_key",
|
|
76
|
-
)
|
|
77
|
-
agent = CodeAgent(
|
|
78
|
-
tools=[SimpleDuckDuckGoSearchTool()],
|
|
79
|
-
model=model,
|
|
80
|
-
)
|
|
81
|
-
agent.run("What are the latest AI news in 2026?")
|
|
82
|
-
|
|
83
|
-
### With Ollama (local)
|
|
84
|
-
|
|
85
|
-
model = LightModel(
|
|
86
|
-
model_id="llama3",
|
|
87
|
-
api_base="http://localhost:11434/v1",
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
### With any custom URL
|
|
91
|
-
|
|
92
|
-
model = LightModel(
|
|
93
|
-
model_id="my-model",
|
|
94
|
-
api_base="https://my-server.example.com/v1",
|
|
95
|
-
api_key="my-key",
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
## API reference
|
|
99
|
-
|
|
100
|
-
### `LightModel`
|
|
101
|
-
|
|
102
|
-
LightModel(
|
|
103
|
-
model_id: str,
|
|
104
|
-
api_base: str,
|
|
105
|
-
api_key: str = "not-needed",
|
|
106
|
-
temperature: float = 0.7,
|
|
107
|
-
max_tokens: int = 2048,
|
|
108
|
-
timeout: int = 120,
|
|
109
|
-
extra: dict | None = None,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
- `model_id`: model name sent to the API.
|
|
113
|
-
- `api_base`: base URL. `/chat/completions` is appended automatically.
|
|
114
|
-
- `api_key`: bearer token (optional for local servers).
|
|
115
|
-
- `extra`: extra JSON fields merged into the request payload.
|
|
116
|
-
|
|
117
|
-
### `SimpleDuckDuckGoSearchTool`
|
|
118
|
-
|
|
119
|
-
SimpleDuckDuckGoSearchTool()
|
|
120
|
-
|
|
121
|
-
Scrapes DuckDuckGo HTML results and returns the top 5 as formatted text.
|
|
122
|
-
No API key required. No Rust dependency.
|
|
123
|
-
|
|
124
|
-
## License
|
|
125
|
-
|
|
126
|
-
MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|