Nothing matches those filters.

Newsletter

1
15:01

AdalFlow: A PyTorch-Like Framework to Auto-Optimizing Prompt for your LLM agent

AdalFlow is a new open-source framework that lets developers auto-optimize their AI prompts instead of tuning them by hand. Built by SylphAI and led by CEO Li Yin, it uses PyTorch-like components, Jinja2 templating, and a technique called LLM-AutoDiff that the company says beats other automatic prompt-optimization libraries on accuracy. It also ships ReAct-style agents that combine planning with tool use, and the post walks through examples including a Q&A pipeline, structured classification, and a LinkedIn outreach agent.

Notes
AdalFlow: PyTorch-like auto-prompt-optimization framework

Context. Guest post by Aria Shi (Developer Relations lead at SylphAI) in The AiEdge Newsletter (2025-09-29), introducing Li Yin's (SylphAI CEO) open-source library AdalFlow (GitHub: SylphAI-Inc/AdalFlow). Vendor-authored, promotional tone — performance claims below are the company's own and unverified.

Core claims / philosophy

  • Treats prompts as "the new programming primitives" — "If PyTorch turned tensors into the lingua franca of deep learning, AdalFlow treats prompts as the new programming primitives." Positioning: "the PyTorch for prompts."
  • Auto-optimization via two research systems: LLM-AutoDiff and Learn-to-Reason. Claims: "our framework achieves the highest accuracy among all automatic prompt optimization libraries."
  • Prompt templates use Jinja2 engine for composable, debuggable prompts; supports zero-shot and few-shot.
  • 100% source-code control/visibility as the pitch for both researchers and production engineers.

Component abstraction (analog of nn.Module):

  • Component — base class, both training (forward) and inference (call) modes; bicall bridges them.
  • GradComponent — supports backprop (e.g., Generator, Retriever).
  • DataComponent — formatting/parsing (e.g., DataClassParser); @adal.func_to_data_component wraps a plain Python function into one.
  • LossComponent — wraps evaluation metrics to give "gradient-like feedback" for text optimization.

Example 1 — Q&A with object counting. Template uses {{system_prompt}}/{{input_str}} placeholders; parse_integer_answer regex-extracts the last number and returns it. adal.Parameter(... requires_opt=True, param_type=ParameterType.PROMPT) marks a prompt for later auto-optimization. Pipeline: Prompt → LLM Generation → Structured Output Parsing → Final Numerical Answer.

Example 2 — Structured classification (TREC). TRECExtendedData(adal.DataClass) with Pydantic-like schema enforcement: __input_fields__ = ["question"], __output_fields__ = ["rationale", "class_name"], classes restricted to ["ABBR","ENTY","DESC","HUM","LOC","NUM"]. DataClassParser(..., format_type="yaml") enforces structured YAML output so "the model never drifts into free-form answers."

Example 3 — Training. eval_fn = AnswerMatchAcc(type="exact_match").compute_single_item; wrapped by adal.EvalFnToTextLoss(...) as a LossComponent → loop: Forward pass → Eval metric → Backward engine → Prompt optimization.

Agents — ReAct paradigm. Two-part architecture:

  • Agent = Generator-based planner + ToolManager (tool registry). Knows available tools and how to call them.
  • Runner = execution loop: multi-step orchestration, tool calling, observation handling, timeouts, final-answer synthesis.

ReAct loop: Plan → Select Tool → Execute → Observe → Iterate (up to max_steps) → Final Answer.

Minimal agent example

```python

agent = Agent(

name="CalculatorAgent",

tools=[calculator], # plain callables or FunctionTool

model_client=OpenAIClient(),

model_kwargs={"model": "gpt-4o", "temperature": 0.3},

max_steps=6,

)

runner = Runner(agent=agent)

result = runner.call(prompt_kwargs={"input_str": "Invoke the calculator tool and calculate 15 * 7 + 23"})

```

  • Backend swap: AnthropicAPIClient with model: "claude-3-sonnet-20240229" shown as drop-in alternative.
  • RunnerResult: .step_history (per-step action+observation), .answer (str or structured type), .error, .ctx.
  • Caveat: the sample tool uses Python's eval() on an LLM-chosen expression string — unsafe for production unless sandboxed.

Advanced/production features

  • Streaming execution: runner.stream(...) yields partial thoughts/tool calls/observations (marked as pseudocode: "actual API may differ slightly in your version").
  • Human-in-the-loop: subclass adal.permissions.PermissionManager, override approve(tool_name, args); example policy allows only "calculator".
  • Custom planner persona via role_desc on Agent.
  • Tracing: from adalflow.tracing import enable_tracing; enable_tracing(project="...") → inspect step_history, tool IO, latency, errors.

LinkedIn recruitment agent (real-world case). Claimed time: manual = 2–3 hours per role; with agent = 10 minutes per role. CLI: linkedin-agent --query "Product Manager" --limit 10. Uses Chrome DevTools Protocol (CDP) browser automation. Five tools: SmartCandidateSearchTool, ExtractCandidateProfilesTool, CandidateEvaluationTool, CandidateOutreachGenerationTool, SaveOutreachResultsTool. Global-state architecture shares partial data (search results → profiles → evaluations → outreach drafts) between tools. LinkedInAgent wraps Agent+Runner, exposes sync call() and async acall(). Output: JSON/CSV with name, title, location, profile_url, score, outreach_message.

Reported run (self-reported, promotional): 2 candidates found (despite --limit 10), 68.8 seconds duration, session 20250909_220617, log files written to logs/workflow_<session>.log (+ debug/agent_steps/errors variants).

Caveats & limitations

  • Vendor-authored promotional piece; "highest accuracy" benchmark claim carries no benchmark names, datasets, or baselines.
  • Streaming API explicitly acknowledged as possibly inaccurate.
  • Case study shows only 2 of 10 requested candidates returned; no data on success rate, LinkedIn ToS compliance, or profile-scraping reliability.
  • eval()-based calculator is illustrative, not production-safe.
  • No license, version number, or install instructions given in the article.
Full text · 21,356 chars
AI Agent frameworks are becoming just as important as model training itself! I am excited to introduce you to Li Yin. She is the CEO of SylphAI and the founder of AdalFlow, a PyTorch-like open-source library on GitHub that enables developers to build and auto-optimize any Language Model (LM) workflows. In this guest post, Aria Shi, the Developer Relations lead at SylphAI, walks you through how AdalFlow empowers AI Agent development, highlighting a hands-on example with a LinkedIn Reachout Agent. Say goodbye to manual prompt engineering. AdalFlow is the all-in-one, auto-differentiative solution for optimizing prompts, whether you’re using zero-shot or few-shot learning. Backed by our state-of-the-art research (LLM-AutoDiff and Learn-to-Reason), our framework achieves the highest accuracy among all automatic prompt optimization libraries. The rise of large language models has completely changed the way we build applications—whether it’s chatbots, RAG systems, or fully autonomous agents. But as an AI engineer, trying to bring these models into production often feels like stitching together a bunch of experiments, rather than building a stable and reliable system. We introduce AdalFlow: a PyTorch-like library designed to bring structure, clarity, and optimization to the world of LLM application development. Built as a community-driven project, AdalFlow is uniting AI research and production engineering into a single ecosystem. Why We Built AdalFlow Modern AI development faces a paradox. On one hand, researchers push the boundaries of model capabilities with new techniques in prompting, evaluation, and optimization. On the other hand, production teams need reproducibility, scalability, and a way to iterate safely on real-world data. Most libraries excel at one side of the equation but leave the other underserved. AdalFlow was born to bridge this gap. With 100% control and clarity of source code, it empowers researchers to experiment freely while giving product engineers the tools to build and ship with confidence. Why AdalFlow Matters By treating prompts as first-class citizens and introducing LLM-AutoDiff, AdalFlow provides what’s been missing in the LLM ecosystem: - For researchers: A familiar PyTorch-like environment to prototype new prompting and training methods. - For engineers: Production-ready workflows that are debuggable, reproducible, and optimizable. - For teams: A shared framework that unites research and production into one healthy ecosystem. Core Philosophy: Prompt Is the New Programming Language If PyTorch turned tensors into the lingua franca of deep learning, AdalFlow treats prompts as the new programming primitives. Every LLM application boils down to structured prompts and their transformations. AdalFlow embraces this reality by making prompt engineering explicit and optimizable. Behind the scenes, it uses the Jinja2 templating engine to let developers define composable prompt structures, ensuring that LLM apps are both modular and debuggable. Components: The Building Blocks of LLM Workflows At the heart of AdalFlow lies the Component abstraction. Just as nn.Module became the foundation for PyTorch models, Components unify every stage of an LLM pipeline. - Component: The base class for all workflows. Handles both training (forward) and inference (call) modes, with bicall bridging the two. - GradComponent: Components capable of backpropagation (e.g., Generators, Retrievers). - DataComponent: Lightweight components for formatting and parsing data (e.g., DataClassParser). - LossComponent: Wraps evaluation metrics and enables gradient-like feedback for text optimization. Example 1: Q&A with Object Counting (Component + DataComponent) template = r"""<START_OF_SYSTEM_PROMPT> {{system_prompt}} <END_OF_SYSTEM_PROMPT> <START_OF_USER> {{input_str}} <END_OF_USER>""" @adal.func_to_data_component def parse_integer_answer(answer: str): numbers = re.findall(r"\d+", answer) return int(numbers[-1]) What’s happening here? - parse_integer_answer is wrapped with@adal.func_to_data_component . - This turns a plain Python function into a DataComponent , which handles structured output parsing. - In this case, it ensures the model’s answer ends with a numerical value. Next, we define a full pipeline: class ObjectCountTaskPipeline(adal.Component): def __init__( self, model_client: adal.ModelClient, model_kwargs: Dict ): super().__init__() system_prompt = adal.Parameter( data="You will answer a reasoning question. Think step by step. The last line should be 'Answer: $VALUE'.", role_desc="Task instruction for the model", requires_opt=True, param_type=ParameterType.PROMPT, ) self.llm_counter = adal.Generator( model_client=model_client, model_kwargs=model_kwargs, template=template, prompt_kwargs={"system_prompt": system_prompt}, output_processors=parse_integer_answer, ) def bicall(self, question: str, id: str = None): return self.llm_counter( prompt_kwargs={"input_str": question}, id=id ) ObjectCountTaskPipeline subclasses Component. Inside it, we define: - A Parameter of type PROMPT , which AdalFlow can later auto-optimize. - A Generator (a GradComponent ) that executes the prompt, then passes the raw LLM output through ourparse_integer_answerDataComponent . The workflow is: Prompt → LLM Generation → Structured Output Parsing → Final Numerical Answer. Example 2: Classification with Structured Output (Component + DataClass) Classification tasks are a perfect showcase of AdalFlow’s DataClass feature. @dataclass class TRECExtendedData(adal.DataClass): question: str = field( metadata={"desc": "The question to be classified"} ) rationale: str = field( metadata={"desc": "Step-by-step reasoning"}, default=None ) class_name: Literal[ "ABBR", "ENTY", "DESC", "HUM", "LOC", "NUM" ] = field( metadata={"desc": "The class name"}, default=None ) __input_fields__ = ["question"] __output_fields__ = ["rationale", "class_name"] - TRECExtendedData extendsDataClass , which (like Pydantic) gives us schema enforcement. - Input: a question. - Output: a rationale (reasoning trace) and a class_name (final label). Now let’s plug it into a pipeline: class TRECClassifierStructuredOutput(adal.Component): def __init__( self, model_client: adal.ModelClient, model_kwargs: Dict ): super().__init__() # Task description prompt task_desc_str = adal.Prompt( template=task_desc_template, prompt_kwargs={ "classes": [ {"label": l, "desc": d} for l, d in zip(_COARSE_LABELS, _COARSE_LABELS_DESC) ] } )() parser = adal.DataClassParser( data_class=TRECExtendedData, return_data_class=True, format_type="yaml" ) prompt_kwargs = { "system_prompt": adal.Parameter( data=task_desc_str, role_desc="Task description", requires_opt=True, param_type=adal.ParameterType.PROMPT, ), "output_format_str": parser.get_output_format_str(), } self.llm = adal.Generator( model_client=model_client, model_kwargs=model_kwargs, prompt_kwargs=prompt_kwargs, template=template, output_processors=parser, ) def bicall(self, question: str, id: Optional[str] = None): return self.llm(prompt_kwargs={"input_str": question}, id=id) - The Prompt defines the system instruction with class definitions. - DataClassParser enforces structured YAML output that matchesTRECExtendedData . - Generator (a GradComponent) runs the LLM with prompt + parser. - Output is guaranteed to follow the schema: rationale + class name. This ensures the model never drifts into free-form answers—it always returns structured classification results. Example 3: Training With LossComponent Finally, how do we train or optimize these components? That’s where LossComponent comes in: eval_fn = AnswerMatchAcc(type="exact_match").compute_single_item loss_fn = adal.EvalFnToTextLoss( eval_fn=eval_fn, eval_fn_desc="exact_match: 1 if str(y) == str(y_gt) else 0" ) - AnswerMatchAcc is the evaluation metric. - EvalFnToTextLoss wraps it as aLossComponent , enabling LLM-AutoDiff to optimize prompts automatically during training. By attaching this to your pipeline, you get a full training loop: Forward pass → Eval metric → Backward engine → Prompt optimization. Agents: Reasoning Meets Action AdalFlow embraces the ReAct paradigm—combining reasoning (plan) with acting (tool use)—to build autonomous, auditable AI systems. An agent reasons about the task, selects tools, executes them, observes results, and iterates until it can deliver a final answer. - https://adalflow.sylph.ai/new_tutorials/agents_runner.html - https://colab.research.google.com/github/SylphAI-Inc/AdalFlow/blob/main/notebooks/agents/agent_tutorial.ipynb Architecture at a Glance - Agent (planner + tool manager) Handles planning and decision-making via a Generator-based planner, and knows what tools are available and how to call them. - Runner (executor + conversation loop) Orchestrates multi-step execution, tool calling, observation handling, timeouts, and final answer synthesis. This separation lets you swap or customize planning vs. execution independently. Execution Flow (ReAct Loop Recap) - Planning – The Agent (Generator planner) analyzes input and proposes the next action. - Tool Selection – Chooses a tool from the registered set. - Tool Execution – The Runner invokes the tool with arguments. - Observation – The result is fed back to the planner. - Iteration – Repeat 1–4 up to max_steps or until confident. - Final Answer – The planner synthesizes the answer (optionally into a structured type). Minimal, End-to-End Example 1) Define a Tool (callable or FunctionTool) # Tool: a plain Python callable works, or wrap with FunctionTool for extras. def calculator(expression: str) -> str: """Evaluate a mathematical expression.""" try: result = eval(expression) return f"Result: {result}" except Exception as e: return f"Error: {e}" 2) Build the Agent (Planner + Tools) from adalflow import Agent, Runner from adalflow.components.model_client.openai_client import OpenAIClient agent = Agent( name="CalculatorAgent", # Agent identifier tools=[calculator], # List of tools (callables or FunctionTool) # LLM client used by the planner (Generator-based) model_client=OpenAIClient(), model_kwargs={"model": "gpt-4o", "temperature": 0.3}, max_steps=6, # Upper bound for ReAct loops ) What this maps to: - Planner: An internal Generator that decides the next step (think: “reasoning trace”). - ToolManager : The agent’s registry of permitted tools. - max_steps: Safety rail to prevent runaway loops. Model Configuration (Swap Backends Easily) # OpenAI from adalflow.components.model_client.openai_client import OpenAIClient agent = Agent( model_client=OpenAIClient(), model_kwargs={"model": "gpt-4o"} ) # Anthropic from adalflow.components.model_client.anthropic_client import ( AnthropicAPIClient ) agent = Agent( model_client=AnthropicAPIClient(), model_kwargs={"model": "claude-3-sonnet-20240229"} ) 3) Execute with the Runner (Multi-step Orchestration) # Manages turns, tool calls, observations, and finalization runner = Runner(agent=agent) result = runner.call( prompt_kwargs={"input_str": "Invoke the calculator tool and calculate 15 * 7 + 23"} ) print(result.answer) # -> "The result of 15 * 7 + 23 is 128." RunnerResult schema (returned by Runner.call) # result has: # - result.step_history: [StepOutput(...)] # Each step’s action + observation # - result.answer: str | structured type # Final synthesized answer # - result.error: None | Exception info # Error if something failed # - result.ctx: dict | None # Optional execution metadata This is the full ReAct loop in action: Plan → Select Tool → Execute → Observe → Iterate → Answer. Advanced Features (Production-Ready) 1) Streaming Execution (Real-Time Updates) # Pseudocode: actual API may differ slightly in your version. stream = runner.stream( prompt_kwargs={"input_str": "Compute 42 * 73 and explain."} ) for update in stream: # update contains partial thoughts, tool calls, observations, etc. print(update) Use streaming to surface live reasoning/tool progress in UIs. 2) Human-in-the-Loop (Permission Management) from adalflow.permissions import PermissionManager class MyPerms(PermissionManager): def approve(self, tool_name: str, args: dict) -> bool: # Example policy: only allow calculator; prompt user otherwise return tool_name == "calculator" agent = Agent( name="GuardedAgent", tools=[calculator, search_tool], model_client=OpenAIClient(), model_kwargs={"model": "gpt-4o"}, permission_manager=MyPerms(), # <- Every tool call can be inspected/approved) Great for tools that hit external systems (files, emails, APIs). 3) Custom System Templates (Planner Behavior) custom_role_desc = """ You are a careful, step-by-step data analyst. When you use a tool, explain why and what you expect to get. """ agent = Agent( name="DataAnalyst", # Custom planner persona and guardrails role_desc=custom_role_desc, model_client=OpenAIClient(), model_kwargs={"model": "gpt-4o", "temperature": 0.2}, ) 4) Tracing (Observability) # Configure tracing once (destination: console, file, or tracing backend) from adalflow.tracing import enable_tracing enable_tracing(project="adalflow-agents-demo") result = runner.call(prompt_kwargs={ "input_str": "Use the calculator for 88*19." }) # Inspect step_history, tool IO, latency, errors, etc. Agent Summary: - Agent = Reasoning + Tool selection (Generator-based planner + ToolManager) - Runner = Controlled execution loop (steps, tools, observations, final answer) - Tools = Safe, permissioned extensions to the agent’s capabilities - Production = Streaming, human approvals, tracing, structured outputs Real-World Use Case: LinkedIn Recruitment Agent with AdalFlow Hiring top talent is one of the most resource-intensive parts of building a company. Recruiters spend hours scrolling LinkedIn, opening profiles, copying notes, and crafting outreach messages. What if we could automate that entire workflow—turning hours of manual searching into minutes of AI-assisted sourcing? That’s exactly what we built using AdalFlow’s Agent + Runner architecture combined with browser automation via Chrome DevTools Protocol (CDP). ✨ Before vs. After Traditional Recruiting Workflow (Manual) ❌ BEFORE: 2–3 hours per role 1. Navigate to LinkedIn people search 2. Type in “Product Manager, San Francisco” 3. Scroll endlessly, click into profiles 4. Skim experience, education, skills 5. Take notes in spreadsheets 6. Write & send DMs manually Automated Workflow with AdalFlow (Agentic) ✅ AFTER: 10 minutes per role 1. Run: linkedin-agent --query “Product Manager” --limit 10 2. Agent plans and executes: - Smart search strategy - Extract profiles via browser automation - Evaluate candidates with scoring models - Draft personalized outreach messages 3. Get structured output: JSON/CSV with names, titles, LinkedIn URLs, evaluation scores, outreach drafts. Recruiters get to focus on talking to people, not copy-pasting data. How It Works — Global State Architecture We structured the solution around a global state shared between tools. Each tool contributes partial data (search results, profiles, evaluations, outreach drafts), which the Agent combines into a full pipeline. Agent combines into a full pipeline. Implementation with AdalFlow We implemented the LinkedInAgent by encapsulating: Agent → Planner + Tools (search, extract, evaluate, outreach) Runner → Execution loop with error handling and logging class LinkedInAgent: """ LinkedIn recruitment agent powered by AdalFlow. - Encapsulates Agent + Runner - Provides default recruitment tools - Supports both sync call() and async acall() """ def __init__( self, model_client: Optional[OpenAIClient] = None, model_kwargs: Optional[Dict[str, Any]] = None, max_steps: Optional[int] = None, role_desc: Optional[str] = None, **kwargs, ): # Defaults model_client = model_client or OpenAIClient() model_kwargs = model_kwargs or { "model": "gpt-4o", “temperature”: 0.3 } max_steps = max_steps or 6 # Recruitment workflow tools self.tools = [ # 1. Search LinkedIn via CDP SmartCandidateSearchTool, # 2. Extract structured profile data ExtractCandidateProfilesTool, # 3. Score candidates CandidateEvaluationTool, # 4. Draft personalized outreach CandidateOutreachGenerationTool, # 5. Persist results SaveOutreachResultsTool, ] # Agent role description (personality / instructions) role_desc = role_desc or "You are a recruitment assistant that sources and evaluates LinkedIn candidates." # Initialize Agent + Runner self.agent = Agent( name="LinkedInRecruiter", tools=self.tools, model_client=model_client, model_kwargs=model_kwargs, max_steps=max_steps, role_desc=role_desc, **kwargs, ) self.runner = Runner(agent=self.agent, max_steps=max_steps) def call( self, query: str, context: Optional[Dict[str, Any]] = None ): return self.runner.call(prompt_kwargs={"input_str": query}) async def acall( self, query: str, context: Optional[Dict[str, Any]] = None ): return await self.runner.acall( prompt_kwargs={"input_str": query} ) Full Workflow Execution Here’s how we stitch the agent into a production workflow: def execute_search_workflow( self, progress_tracker=None) -> List[Dict[str, Any]]: logger = get_logger() logger.set_workflow_context("workflow_main", "initialization") log_phase_start("WORKFLOW_START", f"Target: {self.limit} candidates for {self.query} in {self.location}") candidates = [] try: log_info("🤖 Initializing LinkedIn agent...") agent, user_query = self.initialize_agent() if progress_tracker: progress_tracker.start_workflow() # Run full pipeline result = agent.call(query=user_query) self._print_agent_execution_steps(result) # Collect data from global state from ..core.workflow_state import get_complete_workflow_data workflow_data = get_complete_workflow_data() candidates = self._build_complete_candidate_data(workflow_data) log_info(f"✅ Found {len(candidates)} candidates") return candidates except Exception as e: log_error(f"❌ Workflow failed: {e}") return candidates Example Output After running: linkedin-agent --query “Product Manager San Francisco” --limit 10 We get structured results like: [ { “name”: “Alex Chen”, “title”: “Senior Product Manager @ Stripe”, “location”: “San Francisco Bay Area”, “profile_url”: “https://linkedin.com/in/alexchen”, “score”: 0.92, “outreach_message”: “Hi Alex, I came across your experience at Stripe...” }, { “name”: “Maria Lopez”, “title”: “PM, Growth @ Airbnb”, “location”: “San Francisco Bay Area”, “profile_url”: “https://linkedin.com/in/marialopez”, “score”: 0.88, “outreach_message”: “Hi Maria, your background in growth product design really stood out...” } ] ============================================================ WORKFLOW COMPLETION SUMMARY ============================================================ Success: ✅ Yes Total Candidates: 2 Duration: 68.8 seconds Session: 20250909_220617 Log Files: • Main: logs/workflow_20250909_220617.log • Debug: logs/debug_20250909_220617.log • Agent Steps: logs/agent_steps_20250909_220617.log • Errors: logs/errors_20250909_220617.log ============================================================ 🏁 MAIN ✅ COMPLETED - Processed 2 candidates [RESULTS] ✅ Recruitment workflow completed! [RESULTS] 📊 Final result: Successfully processed 2 candidates Looking Ahead As the LLM landscape evolves, frameworks like AdalFlow will become the backbone of application development. Just as PyTorch accelerated deep learning, AdalFlow has the potential to democratize LLM app building—from chatbots to agents to beyond. If you’re excited about shaping the future of AI workflows, the project is open-source and community-driven. Whether you’re an AI researcher, product engineer, or just curious about building smarter applications, now’s the time to get involved. 🚀 AdalFlow isn’t just another library. It’s a paradigm shift in how we think about programming with language models.