lightmodel 0.2.0__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
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lightmodel
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Ultra-light autonomous AI agents for any OpenAI-compatible API
5
- Author-email: Your Name <you@example.com>
5
+ Author-email: PlimbAI <plimb.ai.contact@gmail.com>
6
6
  License-Expression: MIT
7
- Keywords: llm,ai,agent,openai,nvidia-nim,tool-calling
7
+ Keywords: llm,ai,agent,openai,nvidia-nim,tool-calling,no-dependencies
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
@@ -20,10 +20,18 @@ Dynamic: license-file
20
20
  Ultra-light **autonomous AI agents** for any OpenAI-compatible API.
21
21
  No `smolagents`, no `litellm`, no `openai` — just `requests`.
22
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
+
23
31
  ## Installation
24
32
 
25
33
  pip install lightmodel
26
- pip install lightmodel[search] # with beautifulsoup4 for web search
34
+ pip install lightmodel[search] # with beautifulsoup4 for web tools
27
35
 
28
36
  ## Quick start
29
37
 
@@ -38,21 +46,47 @@ No `smolagents`, no `litellm`, no `openai` — just `requests`.
38
46
  model=model,
39
47
  tools=[SimpleDuckDuckGoSearchTool()],
40
48
  system_prompt="You are a helpful research assistant.",
49
+ max_steps=6,
50
+ verbose=True,
41
51
  )
42
52
  print(agent.run("What are the latest AI news in 2026?"))
43
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
+
44
59
  ## Custom tools
45
60
 
46
61
  from lightmodel import Tool
47
62
 
48
63
  class CalculatorTool(Tool):
49
64
  name = "calculator"
50
- description = "Evaluate a basic arithmetic expression."
65
+ description = "Evaluate arithmetic."
51
66
  inputs = {"expression": {"type": "string", "description": "e.g. 2+2"}}
52
67
 
53
68
  def forward(self, expression):
54
69
  return str(eval(expression, {"__builtins__": {}}, {}))
55
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
+
56
90
  ## License
57
91
 
58
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,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lightmodel"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Ultra-light autonomous AI agents for any OpenAI-compatible API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "MIT"
12
- authors = [{name = "Your Name", email = "you@example.com"}]
13
- keywords = ["llm", "ai", "agent", "openai", "nvidia-nim", "tool-calling"]
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
16
  "Operating System :: OS Independent",
@@ -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}"
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lightmodel
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Ultra-light autonomous AI agents for any OpenAI-compatible API
5
- Author-email: Your Name <you@example.com>
5
+ Author-email: PlimbAI <plimb.ai.contact@gmail.com>
6
6
  License-Expression: MIT
7
- Keywords: llm,ai,agent,openai,nvidia-nim,tool-calling
7
+ Keywords: llm,ai,agent,openai,nvidia-nim,tool-calling,no-dependencies
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
@@ -20,10 +20,18 @@ Dynamic: license-file
20
20
  Ultra-light **autonomous AI agents** for any OpenAI-compatible API.
21
21
  No `smolagents`, no `litellm`, no `openai` — just `requests`.
22
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
+
23
31
  ## Installation
24
32
 
25
33
  pip install lightmodel
26
- pip install lightmodel[search] # with beautifulsoup4 for web search
34
+ pip install lightmodel[search] # with beautifulsoup4 for web tools
27
35
 
28
36
  ## Quick start
29
37
 
@@ -38,21 +46,47 @@ No `smolagents`, no `litellm`, no `openai` — just `requests`.
38
46
  model=model,
39
47
  tools=[SimpleDuckDuckGoSearchTool()],
40
48
  system_prompt="You are a helpful research assistant.",
49
+ max_steps=6,
50
+ verbose=True,
41
51
  )
42
52
  print(agent.run("What are the latest AI news in 2026?"))
43
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
+
44
59
  ## Custom tools
45
60
 
46
61
  from lightmodel import Tool
47
62
 
48
63
  class CalculatorTool(Tool):
49
64
  name = "calculator"
50
- description = "Evaluate a basic arithmetic expression."
65
+ description = "Evaluate arithmetic."
51
66
  inputs = {"expression": {"type": "string", "description": "e.g. 2+2"}}
52
67
 
53
68
  def forward(self, expression):
54
69
  return str(eval(expression, {"__builtins__": {}}, {}))
55
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
+
56
90
  ## License
57
91
 
58
92
  MIT
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  src/lightmodel/__init__.py
5
5
  src/lightmodel/agent.py
6
+ src/lightmodel/exceptions.py
6
7
  src/lightmodel/model.py
7
8
  src/lightmodel/tools.py
8
9
  src/lightmodel.egg-info/PKG-INFO
@@ -1,41 +0,0 @@
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
- ## Installation
7
-
8
- pip install lightmodel
9
- pip install lightmodel[search] # with beautifulsoup4 for web search
10
-
11
- ## Quick start
12
-
13
- from lightmodel import Agent, LightModel, SimpleDuckDuckGoSearchTool
14
-
15
- model = LightModel(
16
- model_id="meta/llama-3.3-70b-instruct",
17
- api_base="https://integrate.api.nvidia.com/v1",
18
- api_key="your_key",
19
- )
20
- agent = Agent(
21
- model=model,
22
- tools=[SimpleDuckDuckGoSearchTool()],
23
- system_prompt="You are a helpful research assistant.",
24
- )
25
- print(agent.run("What are the latest AI news in 2026?"))
26
-
27
- ## Custom tools
28
-
29
- from lightmodel import Tool
30
-
31
- class CalculatorTool(Tool):
32
- name = "calculator"
33
- description = "Evaluate a basic arithmetic expression."
34
- inputs = {"expression": {"type": "string", "description": "e.g. 2+2"}}
35
-
36
- def forward(self, expression):
37
- return str(eval(expression, {"__builtins__": {}}, {}))
38
-
39
- ## License
40
-
41
- MIT
@@ -1,7 +0,0 @@
1
- """LightModel - Ultra-light autonomous AI agents."""
2
- from .model import LightModel
3
- from .tools import Tool, SimpleDuckDuckGoSearchTool
4
- from .agent import Agent
5
-
6
- __version__ = "0.2.0"
7
- __all__ = ["LightModel", "Tool", "SimpleDuckDuckGoSearchTool", "Agent"]
@@ -1,71 +0,0 @@
1
- """LightModel Agent - autonomous tool-calling loop."""
2
- import json
3
-
4
-
5
- class Agent:
6
- """Autonomous agent using native OpenAI tool calling."""
7
-
8
- def __init__(self, model, tools=None, system_prompt=None,
9
- max_steps=10, verbose=False):
10
- self.model = model
11
- self.tools = {t.name: t for t in (tools or [])}
12
- self.system_prompt = system_prompt
13
- self.max_steps = max_steps
14
- self.verbose = verbose
15
-
16
- def run(self, task):
17
- messages = []
18
- if self.system_prompt:
19
- messages.append({"role": "system", "content": self.system_prompt})
20
- messages.append({"role": "user", "content": task})
21
-
22
- openai_tools = [t.to_openai() for t in self.tools.values()] or None
23
-
24
- for step in range(1, self.max_steps + 1):
25
- msg = self.model.chat(messages, tools=openai_tools)
26
-
27
- assistant_msg = {"role": "assistant"}
28
- if msg.get("content"):
29
- assistant_msg["content"] = msg["content"]
30
- if msg.get("tool_calls"):
31
- assistant_msg["tool_calls"] = msg["tool_calls"]
32
- messages.append(assistant_msg)
33
-
34
- tool_calls = msg.get("tool_calls") or []
35
- if not tool_calls:
36
- return msg.get("content") or ""
37
-
38
- if self.verbose:
39
- print(f"[step {step}] {len(tool_calls)} tool call(s)")
40
-
41
- for tc in tool_calls:
42
- name = tc["function"]["name"]
43
- raw_args = tc["function"]["arguments"]
44
- try:
45
- args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
46
- except json.JSONDecodeError:
47
- args = {}
48
-
49
- if self.verbose:
50
- print(f" → {name}({args})")
51
-
52
- tool = self.tools.get(name)
53
- if tool is None:
54
- result = f"Unknown tool: {name}"
55
- else:
56
- try:
57
- result = tool.forward(**args)
58
- except Exception as e:
59
- result = f"Tool error: {e}"
60
-
61
- if self.verbose:
62
- preview = str(result)[:120].replace("\n", " ")
63
- print(f" ← {preview}...")
64
-
65
- messages.append({
66
- "role": "tool",
67
- "tool_call_id": tc["id"],
68
- "content": str(result),
69
- })
70
-
71
- return "[max_steps reached without final answer]"
@@ -1,45 +0,0 @@
1
- """LightModel - pure HTTP client for OpenAI-compatible APIs."""
2
- import requests
3
-
4
-
5
- class LightModel:
6
- """Client for any OpenAI-compatible /chat/completions API."""
7
-
8
- def __init__(self, model_id, api_base, api_key="not-needed",
9
- temperature=0.7, max_tokens=2048, timeout=120, extra=None):
10
- self.model_id = model_id
11
- base = api_base.rstrip("/")
12
- if not base.endswith("/chat/completions"):
13
- base += "/chat/completions"
14
- self.api_url = base
15
- self.api_key = api_key
16
- self.temperature = temperature
17
- self.max_tokens = max_tokens
18
- self.timeout = timeout
19
- self.extra = extra or {}
20
-
21
- def chat(self, messages, tools=None):
22
- """Send messages to the API. Returns the assistant message dict."""
23
- headers = {"Content-Type": "application/json"}
24
- if self.api_key:
25
- headers["Authorization"] = f"Bearer {self.api_key}"
26
-
27
- payload = {
28
- "model": self.model_id,
29
- "messages": messages,
30
- "temperature": self.temperature,
31
- "max_tokens": self.max_tokens,
32
- **self.extra,
33
- }
34
- if tools:
35
- payload["tools"] = tools
36
- payload["tool_choice"] = "auto"
37
-
38
- r = requests.post(self.api_url, json=payload, headers=headers,
39
- timeout=self.timeout)
40
- if r.status_code >= 400:
41
- raise RuntimeError(f"API {r.status_code}: {r.text[:300]}")
42
- try:
43
- return r.json()["choices"][0]["message"]
44
- except (KeyError, IndexError):
45
- raise RuntimeError(f"Invalid response: {r.text[:300]}")
@@ -1,61 +0,0 @@
1
- """Lightweight tools for LightModel agents."""
2
- import requests
3
- from bs4 import BeautifulSoup
4
-
5
-
6
- class Tool:
7
- """Base class for all tools. Subclass and override forward()."""
8
- name = ""
9
- description = ""
10
- inputs = {} # {"arg_name": {"type": "string", "description": "..."}}
11
-
12
- def forward(self, **kwargs):
13
- raise NotImplementedError
14
-
15
- def to_openai(self):
16
- props, required = {}, []
17
- for name, spec in self.inputs.items():
18
- props[name] = {
19
- "type": spec.get("type", "string"),
20
- "description": spec.get("description", ""),
21
- }
22
- required.append(name)
23
- return {
24
- "type": "function",
25
- "function": {
26
- "name": self.name,
27
- "description": self.description,
28
- "parameters": {
29
- "type": "object",
30
- "properties": props,
31
- "required": required,
32
- },
33
- },
34
- }
35
-
36
-
37
- class SimpleDuckDuckGoSearchTool(Tool):
38
- """Lightweight DuckDuckGo web search (HTML scraping, no API key)."""
39
- name = "web_search"
40
- description = "Search the web via DuckDuckGo. Returns the top 5 results."
41
- inputs = {
42
- "query": {"type": "string", "description": "The search query."}
43
- }
44
-
45
- def forward(self, query):
46
- url = "https://html.duckduckgo.com/html/"
47
- headers = {"User-Agent": "Mozilla/5.0"}
48
- try:
49
- r = requests.post(url, data={"q": query}, headers=headers, timeout=10)
50
- r.raise_for_status()
51
- soup = BeautifulSoup(r.text, "html.parser")
52
- out = []
53
- for res in soup.select(".result")[:5]:
54
- title = res.select_one(".result__a")
55
- snippet = res.select_one(".result__snippet")
56
- if title and snippet:
57
- out.append(f"- {title.get_text(strip=True)}\n "
58
- f"{title['href']}\n {snippet.get_text(strip=True)}")
59
- return "\n\n".join(out) if out else "No results found."
60
- except Exception as e:
61
- return f"Search error: {e}"
File without changes