Skip to main content

llm-interaction

Multi-provider LLM client with typed tool calling, lazy output parsing, and Azure/Databricks/OpenRouter/LiteLLM/DeepSeek/local backends. Supports both async and sync (notebook-friendly) usage.

Install

pip install llm-interaction

# For Databricks backend:
pip install llm-interaction[databricks]

# For LiteLLM backend (Anthropic, Databricks Claude, Vertex AI, etc.):
pip install llm-interaction[litellm]

Quick Start

Loading Environment Variables

The library reads configuration from environment variables. Use python-dotenv to load them from a .env file:

from dotenv import load_dotenv

# Call this once at the start of your script/notebook
load_dotenv()

Example .env file:

LLM_INTERACTION_API_KEY=your-api-key
LLM_INTERACTION_ENDPOINT=https://your-resource.openai.azure.com
LLM_INTERACTION_MODEL=gpt-4o
LLM_INTERACTION_BACKEND=azure   # optional, defaults to "azure"

If your environment variables are already set (e.g., in production), you can skip load_dotenv().

Sync (Notebook-Friendly)

from pathlib import Path
from llm_interaction import LLMInteraction

# Azure OpenAI (default)
llm = LLMInteraction(prompt_dir=Path("prompts"))

# Databricks
llm = LLMInteraction(prompt_dir=Path("prompts"), backend="databricks")

# OpenRouter
llm = LLMInteraction(
    prompt_dir=Path("prompts"),
    backend="openrouter",
    api_key="your-openrouter-key",
    model="openai/gpt-4",
)

# LiteLLM (universal provider support)
llm = LLMInteraction(prompt_dir=Path("prompts"), backend="litellm")

# DeepSeek
llm = LLMInteraction(prompt_dir=Path("prompts"), backend="deepseek")

# Local (llama-server)
llm = LLMInteraction(prompt_dir=Path("prompts"), backend="local")

# Use sync methods in notebooks (no await needed)
result = llm.sync_query(system="Be helpful", user="Hello")
print(result.text)

# Or with templates
result = llm.sync_query_template(
    prompt_name="greeting",
    variables={"name": "Alice"},
)
print(result.text)

# Or run an agentic loop
from llm_interaction import tool

@tool(stop=True)
def submit_answer(answer: str) -> str:
    """Submit the final answer."""
    return "done"

result = llm.sync_agent_loop(
    system="You are a helpful assistant.",
    user="What is 2+2?",
    tools=[submit_answer],
)
print(f"Tool calls: {result.tool_call_count}, Reason: {result.stop_reason}")

# Or with templates
result = llm.sync_agent_loop_template(
    prompt_name="math",
    variables={"problem": "What is 2+2?"},
    tools=[submit_answer],
)
print(f"Tool calls: {result.tool_call_count}, Reason: {result.stop_reason}")

Async

from pathlib import Path
from llm_interaction import LLMInteraction

llm = LLMInteraction(prompt_dir=Path("prompts"))

# Use async methods in async contexts
result = await llm.query(system="Be helpful", user="Hello")
print(result.text)

# Or with templates
result = await llm.query_template(
    prompt_name="greeting",
    variables={"name": "Alice"},
)
print(result.text)

# Or run an agentic loop
from llm_interaction import tool

@tool(stop=True)
def submit_answer(answer: str) -> str:
    """Submit the final answer."""
    return "done"

result = await llm.agent_loop(
    system="You are a helpful assistant.",
    user="What is 2+2?",
    tools=[submit_answer],
)

# Or with templates
result = await llm.agent_loop_template(
    prompt_name="math",
    variables={"problem": "What is 2+2?"},
    tools=[submit_answer],
)

Output Parsing

Both query() and sync_query() return an LLMResponse. Parsing is lazy — call the method you need:

# Raw text
result.text

# JSON (extracts last ```json block, falls back to json_repair)
data = result.json()

# YAML
data = result.yaml()

# Scratchpad + JSON: splits reasoning text from structured data
scratchpad, data = result.scratchpad_json()
# scratchpad = "Let me analyze this step by step..."
# data = {"topics": ["ai", "ml"]}

# Scratchpad + YAML
scratchpad, data = result.scratchpad_yaml()

# Pydantic model (validates + auto-retries on failure)
from pydantic import BaseModel

class Analysis(BaseModel):
    topics: list[str]
    confidence: float

analysis = await result.parse(Analysis)
# On validation error, re-queries the LLM with the error message
# using previous_response_id for efficient context chaining

Tool Calling

@tool
def search(query: str, max_results: int = 10) -> list[dict]:
    """Search for documents.

    Args:
        query: The search query string
        max_results: Maximum number of results to return
    """
    return db.search(query, limit=max_results)

@tool(stop=True)
def submit(answer: str) -> str:
    """Submit the final answer."""
    return "done"

result = await llm.agent_loop(
    system="You are a research agent.",
    user="Find info about quantum computing.",
    tools=[search, submit],
)

Jinja Templates

Templates use the naming convention {name}_system.jinja and {name}_user.jinja:

# prompts/research_system.jinja
You are a {{ role }} assistant.

# prompts/research_user.jinja
Find information about {{ topic }}.
result = await llm.query_template(
    prompt_name="research",
    variables={"role": "research", "topic": "quantum computing"},
)

Context Injection

class WeatherAPI:
    def get(self, city: str) -> dict:
        return {"city": city, "temp_c": 22, "condition": "sunny"}

@tool
def get_weather(ctx: ToolContext[WeatherAPI], city: str) -> dict:
    """Get current weather for a city.

    Args:
        city: City name to look up
    """
    return ctx.get(city)

weather_api = WeatherAPI()

result = await llm.agent_loop(
    system="You are a helpful assistant with weather access.",
    user="What's the weather in Oslo?",
    tools=[get_weather],
    context=[weather_api],  # matched by type to ToolContext[WeatherAPI]
)

A single tool can use multiple contexts, each matched by type:

class WeatherAPI:
    def get(self, city: str) -> dict:
        return {"city": city, "temp_c": 22, "condition": "sunny"}

class UserPreferences:
    def __init__(self, unit: str = "celsius"):
        self.unit = unit

@tool
def get_weather(
    weather: ToolContext[WeatherAPI],
    prefs: ToolContext[UserPreferences],
    city: str,
) -> str:
    """Get weather for a city in the user's preferred unit.

    Args:
        city: City name to look up
    """
    data = weather.get(city)
    if prefs.unit == "fahrenheit":
        data["temp_f"] = data["temp_c"] * 9 / 5 + 32
    return data

result = await llm.agent_loop(
    system="You are a weather assistant.",
    user="What's the weather in Oslo?",
    tools=[get_weather],
    context=[WeatherAPI(), UserPreferences(unit="fahrenheit")],
)

Environment Variables

Set LLM_INTERACTION_BACKEND to switch providers without changing code (defaults to "azure").

Azure OpenAI (default backend):

LLM_INTERACTION_BACKEND=azure
LLM_INTERACTION_API_KEY=your-api-key
LLM_INTERACTION_ENDPOINT=https://your-resource.openai.azure.com
LLM_INTERACTION_MODEL=gpt-4o

Databricks (backend="databricks"):

LLM_INTERACTION_BACKEND=databricks
LLM_INTERACTION_DATABRICKS_HOST=https://your-workspace.azuredatabricks.net
LLM_INTERACTION_MODEL=your-serving-endpoint

Databricks auth is handled automatically by WorkspaceClient:

  • On-site (notebook): no setup needed
  • Off-site (local dev): run databricks auth login --host <your-host> first

OpenRouter (backend="openrouter"):

LLM_INTERACTION_BACKEND=openrouter
LLM_INTERACTION_API_KEY=your-openrouter-key
LLM_INTERACTION_MODEL=openai/gpt-4

LiteLLM (backend="litellm"):

LLM_INTERACTION_BACKEND=litellm
LLM_INTERACTION_API_KEY=your-provider-key
LLM_INTERACTION_ENDPOINT=https://provider-endpoint   # optional, depends on provider
LLM_INTERACTION_MODEL=databricks/my-claude           # must use litellm's provider/model format

Env vars are automatically forwarded to provider-specific vars (e.g. DATABRICKS_API_KEY, ANTHROPIC_API_KEY).

DeepSeek (backend="deepseek"):

LLM_INTERACTION_BACKEND=deepseek
LLM_INTERACTION_API_KEY=your-deepseek-key
LLM_INTERACTION_MODEL=deepseek-chat

Uses the Chat Completions API (full message history is resent each turn).

Local (backend="local"):

LLM_INTERACTION_BACKEND=local
LLM_INTERACTION_ENDPOINT=http://localhost:8080/v1   # optional, this is the default
LLM_INTERACTION_MODEL=my-model

No API key required. Uses the Responses API (/v1/responses) which llama-server supports natively.

Debugging

Each API call stores the request kwargs and raw response for inspection:

details = llm.last_call
if details:
    print(details["request"])   # {model, input, tools, ...}
    print(details["response"])  # raw SDK response object
    print(details["response"].usage)  # token counts, etc.

Returns None if no call has been made yet.

License

MIT

Download files

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

Source Distribution

llm_interaction-0.1.7.tar.gz (44.9 kB view details)

Uploaded Source

Built Distribution

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

llm_interaction-0.1.7-py3-none-any.whl (24.0 kB view details)

Uploaded Python 3

File details

Details for the file llm_interaction-0.1.7.tar.gz.

File metadata

  • Download URL: llm_interaction-0.1.7.tar.gz
  • Upload date:
  • Size: 44.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.4

File hashes

Hashes for llm_interaction-0.1.7.tar.gz
Algorithm Hash digest
SHA256 3b237cf1755a83c4cd7ebb9108a6a4beac9fb50cb773317258770009ce189cab
MD5 4529c0a3e89de7b10c05554bd1906fba
BLAKE2b-256 173c31bff470781e793cd830983dd305549b8ecc3c9f69d2056e3a922d3be631

See more details on using hashes here.

File details

Details for the file llm_interaction-0.1.7-py3-none-any.whl.

File metadata

File hashes

Hashes for llm_interaction-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 2a757e3a5503ff3515de5d0032308efe3fe2b089d7e44b6d8d51dd6db4cb4ddf
MD5 1153db3631ee05c048ec9f262b480fde
BLAKE2b-256 51bcaf894fe1be98330c5f615274bc90faf810bce5047e27d2bf8c4a39c2c082

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