A super lightweight library for LLM-based applications
Project description
A lightweight Python library for building AI-powered applications with clean function calling, vision support, and MLflow integration.
TinyLoop is fully built on top of LiteLLM, providing 100% compatibility with the LiteLLM API while adding powerful abstractions and utilities. This means you can use any model, provider, or feature that LiteLLM supports, including:
- All LLM Providers: OpenAI, Anthropic, Google, Azure, Cohere, and 100+ more
- All Model Types: Chat, completion, embedding, and vision models
- Advanced Features: Streaming, function calling, structured outputs, and more
- Ops Features: Retries, fallbacks, caching, and cost tracking
TinyLoop provides a clean, intuitive interface for working with Large Language Models (LLMs), featuring:
- ๐ฏ Clean Function Calling: Convert Python functions to JSON tool definitions automatically
- ๐ MLflow Integration: Built-in tracing and monitoring with customizable span names
- ๐๏ธ Vision Support: Handle images and vision models seamlessly
- ๐ Structured Output: Generate structured data from LLM responses using Pydantic
- ๐ Tool Loops: Execute multi-step tool calling workflows
- โก Async Support: Full async/await support for all operations
๐ฆ Installation
pip install tinyloop
๐ Quick Start
Basic LLM Usage
Synchronous Calls
from tinyloop.inference.litellm import LLM
# Initialize the LLM
llm = LLM(model="openai/gpt-3.5-turbo", temperature=0.1)
# Simple text generation
response = llm(prompt="Hello, how are you?")
print(response)
# Get conversation history
history = llm.get_history()
# Access comprehensive response information
print(f"Response: {response}")
print(f"Cost: ${response.cost:.6f}")
print(f"Tool calls: {response.tool_calls}")
print(f"Raw response: {response.raw_response}")
print(f"Message history: {len(response.message_history)} messages")
Asynchronous Calls
from tinyloop.inference.litellm import LLM
llm = LLM(model="openai/gpt-3.5-turbo", temperature=0.1)
# Async text generation
response = await llm.acall(prompt="Hello, how are you?")
print(response)
๐ Tool Loops
Execute multi-step tool calling workflows:
from tinyloop.modules.tool_loop import ToolLoop
from tinyloop.features.function_calling import Tool
from pydantic import BaseModel
import random
def roll_dice():
"""Roll a dice and return the result"""
return random.randint(1, 6)
class FinalAnswer(BaseModel):
last_roll: int
reached_goal: bool
# Create tool loop
loop = ToolLoop(
model="openai/gpt-4.1",
system_prompt="""
You are a dice rolling assistant.
Roll a dice until you get the number indicated in the prompt.
Use the roll_dice function to roll the dice.
Return the last roll and whether you reached the goal.
""",
temperature=0.1,
output_format=FinalAnswer,
tools=[Tool(roll_dice)]
)
# Execute the loop
response = loop(
prompt="Roll a dice until you get a 6",
parallel_tool_calls=False,
)
print(f"Last roll: {response.last_roll}")
print(f"Reached goal: {response.reached_goal}")
Supported Features
๐ฏ Structured Output Generation
Generate structured data using Pydantic models:
from tinyloop.inference.litellm import LLM
from pydantic import BaseModel
from typing import List
class CalendarEvent(BaseModel):
name: str
date: str
participants: List[str]
class EventsList(BaseModel):
events: List[CalendarEvent]
# Initialize LLM with structured output
llm = LLM(
model="openai/gpt-4.1-nano",
temperature=0.1,
)
# Generate structured data
response = llm(
prompt="List 5 important events in the XIX century",
response_format=EventsList
)
# Access structured data
for event in response.events:
print(f"{event.name} - {event.date}")
print(f"Participants: {', '.join(event.participants)}")
๐๏ธ Vision
Work with images using various input methods:
from tinyloop.inference.litellm import LLM
from tinyloop.features.vision import Image
from PIL import Image as PILImage
llm = LLM(model="openai/gpt-4.1-nano", temperature=0.1)
# From PIL Image
pil_image = PILImage.open("image.jpg")
image = Image.from_PIL(pil_image)
# From file path
image = Image.from_file("image.jpg")
# From URL
image = Image.from_url("https://example.com/image.jpg")
# Analyze image
response = llm(prompt="Describe this image", images=[image])
print(response)
๐ง Function Calling
Convert Python functions to LLM tools with automatic schema generation:
from tinyloop.inference.litellm import LLM
from tinyloop.features.function_calling import Tool
import json
def get_current_weather(location: str, unit: str):
"""Get the current weather in a given location
Args:
location: The city and state, e.g. San Francisco, CA
unit: Temperature unit {'celsius', 'fahrenheit'}
Returns:
A sentence indicating the weather
"""
if location == "Boston, MA":
return "The weather is 12ยฐF"
return f"Weather in {location} is sunny"
# Create LLM instance
llm = LLM(model="openai/gpt-4.1-nano", temperature=0.1)
# Create tool from function
weather_tool = Tool(get_current_weather)
# Use function calling
inference = llm(
prompt="What is the weather in Boston, MA?",
tools=[weather_tool],
)
# Process tool calls
for tool_call in inference.raw_response.choices[0].message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
print(f"Tool: {tool_name}")
print(f"Args: {tool_args}")
print(weather_tool(**tool_args))
# Access comprehensive response information
print(f"Total cost: ${inference.cost:.6f}")
print(f"Tool calls made: {len(inference.tool_calls) if inference.tool_calls else 0}")
print(f"Conversation length: {len(inference.message_history)} messages")
๐ Generate Module
Simple text generation with a clean interface:
from tinyloop.modules.generate import Generate
# Synchronous generation
response = Generate.run(
prompt="Write a haiku about programming",
model="openai/gpt-3.5-turbo",
temperature=0.7
)
print(response.response)
# Async generation
response = await Generate.arun(
prompt="Explain quantum computing",
model="openai/gpt-4",
temperature=0.3
)
print(response.response)
# Using the class for multiple calls
generator = Generate(
model="openai/gpt-3.5-turbo",
temperature=0.5,
system_prompt="You are a helpful coding assistant."
)
response1 = generator.call("How do I implement a binary search?")
response2 = generator.call("What's the time complexity?")
๐จ Prompt Rendering
Manage prompts with YAML templates and Jinja2:
from tinyloop.utils.prompt_renderer import PromptRenderer, render_base_prompts
# Using PromptRenderer class
renderer = PromptRenderer("prompts/chat.yaml")
system_prompt = renderer.render("system", user_name="Alice", context="coding")
user_prompt = renderer.render("user", question="How do I debug Python?")
Example YAML prompt file (prompts/chat.yaml):
system: |
You are {{ user_name }}, a helpful AI assistant specializing in {{ context }}.
Always provide clear, actionable advice.
user: |
{{ user_name }}, I have a question: {{ question }}
Please provide a detailed response with examples if relevant.
๐ Streaming Responses
Get real-time responses as they're generated:
from tinyloop.inference.litellm import LLM
llm = LLM(model="openai/gpt-3.5-turbo", temperature=0.1)
# Stream responses
for chunk in llm.stream(prompt="Write a story about a robot"):
print(chunk.response, end="", flush=True)
๐ Async Tool Loops
Execute tool loops asynchronously for better performance:
import asyncio
from tinyloop.modules.tool_loop import ToolLoop
from tinyloop.features.function_calling import Tool
from pydantic import BaseModel
def fetch_data(source: str):
"""Fetch data from a source"""
return f"Data from {source}: [1, 2, 3, 4, 5]"
def process_data(data: str):
"""Process the fetched data"""
return f"Processed: {data}"
class AnalysisResult(BaseModel):
final_result: str
steps_completed: int
async def main():
loop = ToolLoop(
model="openai/gpt-4",
system_prompt="You are a data analyst. Fetch and process data step by step.",
temperature=0.1,
output_format=AnalysisResult,
tools=[Tool(fetch_data), Tool(process_data)]
)
result = await loop.acall(
prompt="Fetch data from 'api' and process it"
)
print(f"Final result: {result.final_result}")
print(f"Steps completed: {result.steps_completed}")
# Run the async function
asyncio.run(main())
๐ Advanced Observability: MLflow Integration
Custom Span Names
Create custom MLflow spans with meaningful names:
from tinyloop.utils.mlflow import mlflow_trace
from tinyloop.features.function_calling import Tool
import mlflow
def get_weather(location: str, unit: str = "celsius"):
"""Get weather for a location"""
return f"Weather in {location}: 20ยฐ{unit}"
def get_stock_price(symbol: str, currency: str = "USD"):
"""Get stock price for a symbol"""
return f"Stock price for {symbol}: $150.00 {currency}"
# Create tools with custom names for better tracing
weather_tool = Tool(get_weather, name="weather_service")
stock_tool = Tool(get_stock_price, name="stock_service")
# Start MLflow run
with mlflow.start_run():
# Call tools - these will create spans with custom names
weather_result = weather_tool("London", "fahrenheit")
stock_result = stock_tool("AAPL", "USD")
# The MLflow spans will be named:
# - "weather_service.__call__" for the weather tool
# - "stock_service.__call__" for the stock tool
Custom Agent Tracing
from tinyloop.utils.mlflow import mlflow_trace
class ResearchAgent:
def __init__(self):
self.llm = LLM(model="openai/gpt-4", temperature=0.1)
@mlflow_trace(mlflow.entities.SpanType.AGENT)
def research_topic(self, topic: str):
"""Research a topic comprehensively"""
response = self.llm(
prompt=f"Research the topic: {topic}. Provide key insights and sources."
)
return response
@mlflow_trace(mlflow.entities.SpanType.AGENT)
def analyze_findings(self, findings: str):
"""Analyze research findings"""
response = self.llm(
prompt=f"Analyze these findings: {findings}. What are the implications?"
)
return response
# Usage with automatic tracing
agent = ResearchAgent()
research_result = agent.research_topic("artificial intelligence")
analysis_result = agent.analyze_findings(research_result.response)
๐ก๏ธ Error Handling and Retries
Handle errors gracefully with retry patterns:
from tinyloop.inference.litellm import LLM
import time
import random
def robust_llm_call(llm, prompt, max_retries=3, delay=1):
"""Make LLM calls with retry logic"""
for attempt in range(max_retries):
try:
response = llm(prompt=prompt)
return response
except Exception as e:
if attempt == max_retries - 1:
raise e
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay * (2 ** attempt) + random.uniform(0, 1))
return None
# Usage
llm = LLM(model="openai/gpt-3.5-turbo", temperature=0.1)
response = robust_llm_call(
llm,
"Explain the concept of machine learning",
max_retries=3
)
print(response.response)
๐ญ Observability (Opt-in)
TinyLoop includes optional tracing integrations. By default, tracing/export is disabled (no-op) to avoid surprise side effects such as:
- Creating local
mlruns/directories - Noisy โException while exporting Spanโ errors when no collector/server is running
You can enable each integration explicitly with environment variables (recommended), or via module parameters where available.
Langfuse / OpenTelemetry (no-op by default)
TinyLoop uses a safe wrapper for Langfuseโs observe decorator. Unless enabled, all @observe(...) decorators are no-ops.
- Enable:
export TINYLOOP_ENABLE_LANGFUSE=1
- Disable (default):
export TINYLOOP_ENABLE_LANGFUSE=0
If you enable Langfuse, make sure your OTEL/Langfuse endpoint is running and configured in your environment.
MLflow (autolog is opt-in)
TinyLoop provides MLflow tracing helpers (e.g. mlflow_trace) and can optionally enable MLflow + LiteLLM autologging.
- Enable MLflow LiteLLM autologging (for
ToolLoop):
export TINYLOOP_ENABLE_MLFLOW=1
- Disable (default):
export TINYLOOP_ENABLE_MLFLOW=0
You can also enable/disable it per ToolLoop instance:
from tinyloop.modules.tool_loop import ToolLoop
loop = ToolLoop(
model="openai/gpt-4.1",
tools=[],
output_format=dict, # example only
enable_mlflow=False, # default is None (use env var)
)
๐ Observability: MLflow Integration
Automatic Tracing
TinyLoop supports MLflow tracing utilities, and (optionally) MLflow + LiteLLM autologging.
- By default, TinyLoop does not enable MLflow autologging at import time (to avoid creating local
mlruns/unexpectedly). - To enable MLflow LiteLLM autologging for
ToolLoop, setTINYLOOP_ENABLE_MLFLOW=1or passenable_mlflow=TruetoToolLoop.
from tinyloop.utils.mlflow import mlflow_trace
class Agent:
@mlflow_trace(mlflow.entities.SpanType.AGENT)
def __call__(self, prompt: str, **kwargs):
self.llm.add_message(self.llm._prepare_user_message(prompt))
for _ in range(self.max_iterations):
response = self.llm(
messages=self.llm.get_history(), tools=self.tools, **kwargs
)
if response.tool_calls:
should_finish = False
for tool_call in response.tool_calls:
tool_response = self.tools_map[tool_call.function_name](
**tool_call.args
)
self.llm.add_message(
self._format_tool_response(tool_call, str(tool_response))
)
if tool_call.function_name == "finish":
should_finish = True
break
if should_finish:
break
return self.llm(
messages=self.llm.get_history(),
response_format=self.output_format,
)
๐๏ธ Project Structure
tinyloop/
โโโ features/
โ โโโ function_calling.py # Function calling utilities
โ โโโ vision.py # Vision model support
โโโ inference/
โ โโโ base.py # Base inference classes
โ โโโ litellm.py # LiteLLM integration
โโโ modules/
โ โโโ base_loop.py # Base loop implementation
โ โโโ generate.py # Generation modules
โ โโโ tool_loop.py # Tool execution loop
โโโ utils/
โโโ mlflow.py # MLflow utilities
๐งช Development
Running Tests
# Run all tests
pytest tests/
# Run specific test file
pytest tests/test_function_calling.py -v
# Run with coverage
pytest tests/ --cov=tinyloop
Examples
Check out the Jupyter notebooks for more detailed examples:
basic_usage.ipynb- Basic usage examplesmodules.ipynb- Advanced module usage
๐ค Contributing
We welcome contributions! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tinyloop-0.1.35.tar.gz.
File metadata
- Download URL: tinyloop-0.1.35.tar.gz
- Upload date:
- Size: 557.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
031af1024b8548e7fc08987a9b75df8a9ca5ce0d97ca6a96163b146e18962294
|
|
| MD5 |
b67482fb88b43699b9b3b0ddbab369ee
|
|
| BLAKE2b-256 |
a8779e116fc1f7a0fb9601665a75a4f42c6fd3abcdcf085106d70eb266badb24
|
File details
Details for the file tinyloop-0.1.35-py3-none-any.whl.
File metadata
- Download URL: tinyloop-0.1.35-py3-none-any.whl
- Upload date:
- Size: 25.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2119234df6ec107893bd5ed43b92801651605fc840b9a66fb83f99c87e0e35da
|
|
| MD5 |
1944cfb239e0421af059d1879d768250
|
|
| BLAKE2b-256 |
a93d5844d6401e6ff9f425fd3e311306f35df301be1b495c379c646ac4f82b26
|