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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Your Name
3
+ Copyright (c) 2026 PlimbAI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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.1.4"
8
- description = "Ultra-light Model adapter and web search tool for smolagents - works with any OpenAI-compatible API"
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 = {text = "MIT"}
12
- authors = [{name = "PlimbAi", email = "plimb.ai.contact@gmail.com"}]
13
- keywords = ["smolagents", "llm", "ai", "agent", "openai", "nvidia-nim", "duckduckgo", "web-search"]
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
@@ -2,6 +2,8 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/lightmodel/__init__.py
5
+ src/lightmodel/agent.py
6
+ src/lightmodel/exceptions.py
5
7
  src/lightmodel/model.py
6
8
  src/lightmodel/tools.py
7
9
  src/lightmodel.egg-info/PKG-INFO
@@ -2,6 +2,3 @@ requests>=2.28
2
2
 
3
3
  [search]
4
4
  beautifulsoup4>=4.12
5
-
6
- [smolagents]
7
- smolagents>=1.0
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
@@ -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,6 +0,0 @@
1
- """LightModel - Ultra-light Model adapter and web search tool for smolagents."""
2
- from .model import LightModel
3
- from .tools import SimpleDuckDuckGoSearchTool
4
-
5
- __version__ = "0.1.0"
6
- __all__ = ["LightModel", "SimpleDuckDuckGoSearchTool"]
@@ -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