Skip to main content

A super lightweight library for LLM-based applications

Project description

tinyLoop Logo

A lightweight Python library for building AI-powered applications with clean function calling, vision support, and MLflow integration.

Python License PyPI

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, set TINYLOOP_ENABLE_MLFLOW=1 or pass enable_mlflow=True to ToolLoop.
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,
    )

tinyLoop Logo

๐Ÿ—๏ธ 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:

๐Ÿค 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.


Made with โค๏ธ for the AI community

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tinyloop-0.1.35.tar.gz (557.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tinyloop-0.1.35-py3-none-any.whl (25.6 kB view details)

Uploaded Python 3

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

Hashes for tinyloop-0.1.35.tar.gz
Algorithm Hash digest
SHA256 031af1024b8548e7fc08987a9b75df8a9ca5ce0d97ca6a96163b146e18962294
MD5 b67482fb88b43699b9b3b0ddbab369ee
BLAKE2b-256 a8779e116fc1f7a0fb9601665a75a4f42c6fd3abcdcf085106d70eb266badb24

See more details on using hashes here.

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

Hashes for tinyloop-0.1.35-py3-none-any.whl
Algorithm Hash digest
SHA256 2119234df6ec107893bd5ed43b92801651605fc840b9a66fb83f99c87e0e35da
MD5 1944cfb239e0421af059d1879d768250
BLAKE2b-256 a93d5844d6401e6ff9f425fd3e311306f35df301be1b495c379c646ac4f82b26

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page