Last active
June 17, 2026 23:55
-
-
Save jezzarax/f450885ba4afae3532907a5aeb8fef99 to your computer and use it in GitHub Desktop.
Example of a simple agentic LLM loop without any agentic frameworks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run | |
| # /// script | |
| # requires-python = ">=3.11" | |
| # dependencies = ["openai", "click"] | |
| # /// | |
| import json | |
| import click | |
| from openai import OpenAI | |
| def create_oai_client(api_key: str, base_url: str | None) -> OpenAI: | |
| client_params = { | |
| "api_key": api_key, | |
| } | |
| if base_url: | |
| client_params["base_url"] = base_url | |
| return OpenAI(**client_params) | |
| DEFAULT_SYSTEM_INSTRUCTION = ( | |
| "You are a helpful cooking assistant. Use find_recipes to look up what " | |
| "can be made with the ingredients the user has on hand, then call " | |
| "get_recipe_steps to retrieve the detailed steps for a recipe that fits. " | |
| "When you have the steps, respond directly with the recipe name, a short " | |
| "intro, and the steps." | |
| ) | |
| DEFAULT_USER_QUERY = ( | |
| "I have chicken thighs, lemons, and thyme in the fridge. " | |
| "What should I make for dinner?" | |
| ) | |
| # Two tools: find_recipes and get_recipe_steps | |
| TOOLS = [ | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "find_recipes", | |
| "description": ( | |
| "Find recipe candidates that match a list of ingredients. " | |
| "Returns a JSON array of recipes with id, name, and short description." | |
| ), | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "ingredients": { | |
| "type": "array", | |
| "items": {"type": "string"}, | |
| "description": "The ingredients the user has on hand.", | |
| }, | |
| }, | |
| "required": ["ingredients"], | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "get_recipe_steps", | |
| "description": "Get the detailed cooking steps for a recipe by id.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "recipe_id": { | |
| "type": "string", | |
| "description": "The id of the recipe to retrieve.", | |
| }, | |
| }, | |
| "required": ["recipe_id"], | |
| }, | |
| }, | |
| }, | |
| ] | |
| # Fake tool implementations | |
| def run_tool(name: str, arguments) -> str: | |
| if name == "find_recipes": | |
| ingredients = arguments.get("ingredients", []) | |
| main = ingredients[0] if ingredients else "ingredient" | |
| joined = ", ".join(ingredients) | |
| recipes = [ | |
| { | |
| "id": "r-001", | |
| "name": f"Pan-seared {main} with citrus", | |
| "description": f"A quick weeknight dish built around {joined}.", | |
| }, | |
| { | |
| "id": "r-002", | |
| "name": f"Braised {main} with herbs", | |
| "description": f"Low-and-slow preparation, about 45 minutes.", | |
| }, | |
| { | |
| "id": "r-003", | |
| "name": f"Roasted {main} tray bake", | |
| "description": f"One-pan recipe, 35 minutes total.", | |
| }, | |
| ] | |
| return json.dumps(recipes) | |
| elif name == "get_recipe_steps": | |
| recipe_id = arguments.get("recipe_id", "r-001") | |
| style = { | |
| "r-001": "pan-seared", | |
| "r-002": "braised", | |
| "r-003": "roasted", | |
| }.get(recipe_id, "pan-seared") | |
| steps = [ | |
| "Pat the main ingredient dry and season with salt and pepper.", | |
| f"Heat a heavy pan or pot to the right temperature for {style} cooking.", | |
| f"Cook the main ingredient using the {style} method until done.", | |
| "Add aromatics (garlic, thyme) and a finishing squeeze of lemon; baste briefly.", | |
| "Rest for a few minutes, then slice and serve.", | |
| ] | |
| return f"Recipe {recipe_id}: " + " ".join(steps) | |
| else: | |
| raise ValueError(f"Unknown tool name: {name}") | |
| def run_agent( | |
| client: OpenAI, model_name: str, system_prompt: str, user_query: str, max_turns=10 | |
| ): | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_query}, | |
| ] | |
| for turn in range(max_turns): | |
| resp = client.chat.completions.create( | |
| model=model_name, messages=messages, tools=TOOLS | |
| ) | |
| msg = resp.choices[0].message | |
| messages.append(msg) | |
| if not msg.tool_calls: | |
| print(f"\n--- Final answer (turn {turn + 1}) ---") | |
| print(msg.content) | |
| return msg.content | |
| print(f"\n--- Turn {turn + 1} ---") | |
| for tc in msg.tool_calls: | |
| name = tc.function.name | |
| args = json.loads(tc.function.arguments) | |
| print(f" Calling {name}({args})") | |
| result = run_tool(name, args) | |
| print(f" -> {result[:100]}...") | |
| # The update rule: append observation to history | |
| messages.append({"role": "tool", "tool_call_id": tc.id, "content": result}) | |
| print("\n[Max turns reached]") | |
| @click.command() | |
| @click.option( | |
| "--openai-model-name", default="openrouter/free", envvar="OPENAI_MODEL_NAME" | |
| ) | |
| @click.option("--openai-api-key", default="sk-none", envvar="OPENAI_API_KEY") | |
| @click.option( | |
| "--openai-base-url", | |
| default="https://openrouter.ai/api/v1", | |
| envvar="OPENAI_BASE_URL", | |
| ) | |
| @click.option("--system-instruction", default=DEFAULT_SYSTEM_INSTRUCTION) | |
| @click.option("--user-query", default=DEFAULT_USER_QUERY) | |
| def main( | |
| openai_model_name: str, | |
| openai_api_key: str, | |
| openai_base_url: str | None, | |
| system_instruction: str, | |
| user_query: str, | |
| ): | |
| client = create_oai_client(openai_api_key, openai_base_url) | |
| run_agent(client, openai_model_name, system_instruction, user_query) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment