Skip to content

I like to build … AI agents & LLM apps#

Weave LLMs into your code at the language level. by llm() turns a function's name, types, and sem annotations into the prompt automatically -- no prompt strings, no response parsing. This track is also a capability you add to any other track: a CLI, a backend, a full-stack app, or a desktop build can all become AI-powered the same way.

Your 5-minute quick win#

Declare what you want through a function signature and let the compiler handle the how. The return type is the output contract:

# The function name, types, and return type ARE the specification
def classify_sentiment(text: str) -> str by llm();

# Enums constrain the LLM to valid outputs
enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
def triage_ticket(description: str) -> Priority by llm();

# obj return types mean every field must be filled -- structured output, no parsing
obj Ingredient { has name: str, cost: float; }
sem Ingredient.cost = "Estimated cost in USD";
def plan_shopping(recipe: str) -> list[Ingredient] by llm();

Enable byLLM with the bundled local model -- no API key, no account, no config:

jac install 'byllm[local]'

That's the whole setup. With no key and no model configured, by llm() runs the bundled local:gemma-4-e4b model in-process (weights download on first call, ~5 GB; local:gemma-4-e2b is a ~2.5 GB alternative). When you want a hosted model, set a provider key and pick one in jac.toml:

export ANTHROPIC_API_KEY="your-key-here"   # or OPENAI_API_KEY, GEMINI_API_KEY, ...
[byllm.model]
default_model = "anthropic/claude-sonnet-4-6"   # any LiteLLM model id works -- ollama/gemma3:4b or local:gemma-4-e4b for free local inference

For agentic workflows, give the LLM tools and let it decide which to call:

def answer_question(question: str) -> str
    by llm(tools=[get_weather, search_web]);

Your learning path#

Going further -- add AI to anything#