Conversational Agent Openai

Conversational agent openai example.

Conversational AI Agent using OpenAI (GPT-4o).

Run it

python examples/ai/conversational_agent_openai.py
"""
Conversational AI Agent using OpenAI (GPT-4o).

This example uses OpenAI's Chat Completions API for production-grade conversations.

Prerequisites:
    1. Get an OpenAI API key: https://platform.openai.com/api-keys
    2. Set the API key as a secret:
       flux secrets set OPENAI_API_KEY "your-api-key"

Usage:
    # Start a new conversation
    flux workflow run conversational_agent_openai '{"message": "Why is the sky blue?"}'

    # Resume the conversation
    flux workflow resume conversational_agent_openai <execution_id> '{"message": "Why does the sky turn red and orange during sunset?"}'

    # Use GPT-4o mini (faster, cheaper)
    flux workflow run conversational_agent_openai '{"message": "What color would the sky be on Mars?", "model": "gpt-4o-mini"}'
"""

from __future__ import annotations

from typing import Any

from openai import AsyncOpenAI

from flux import ExecutionContext, task, workflow
from flux.tasks import pause


@task.with_options(
    secret_requests=["OPENAI_API_KEY"],
    retry_max_attempts=3,
    retry_delay=1,
    retry_backoff=2,
    timeout=60,
)
async def call_openai_api(
    messages: list[dict[str, str]],
    system_prompt: str,
    model: str,
    temperature: float,
    max_tokens: int,
    secrets: dict[str, Any] | None = None,
) -> tuple[str, int]:
    """Call OpenAI API to generate a response using the official SDK."""
    if not secrets or "OPENAI_API_KEY" not in secrets:
        raise ValueError(
            "OPENAI_API_KEY not found. Set it using: flux secrets set OPENAI_API_KEY 'your-key'",
        )

    try:
        client = AsyncOpenAI(api_key=secrets["OPENAI_API_KEY"])

        # Prepare messages with system prompt
        full_messages = [{"role": "system", "content": system_prompt}]
        full_messages.extend(messages)

        # Call OpenAI chat completions API
        response = await client.chat.completions.create(
            model=model,
            messages=full_messages,
            temperature=temperature,
            max_tokens=max_tokens,
        )

        assistant_message = response.choices[0].message.content
        tokens_used = response.usage.total_tokens

        return assistant_message, tokens_used

    except Exception as e:
        raise RuntimeError(f"Failed to call OpenAI API: {str(e)}") from e


@task
async def conversation_turn(
    messages: list[dict[str, str]],
    user_message: str,
    system_prompt: str,
    model: str,
    temperature: float,
    max_tokens: int,
) -> tuple[list[dict[str, str]], str, int]:
    """
    Process one turn of the conversation.

    Args:
        messages: Current conversation history
        user_message: The user's message
        system_prompt: System prompt for the LLM
        model: OpenAI model to use
        temperature: Sampling temperature
        max_tokens: Maximum tokens per response

    Returns:
        Tuple of (updated messages, assistant response, tokens used)
    """
    # Add user message to history
    messages.append({"role": "user", "content": user_message})

    # Call LLM to generate response
    assistant_response, tokens_used = await call_openai_api(
        messages=messages,
        system_prompt=system_prompt,
        model=model,
        temperature=temperature,
        max_tokens=max_tokens,
    )

    # Add assistant response to history
    messages.append({"role": "assistant", "content": assistant_response})

    return messages, assistant_response, tokens_used


@workflow
async def conversational_agent_openai(ctx: ExecutionContext[dict[str, Any]]):
    """
    A conversational AI agent using OpenAI's GPT models.

    Initial Input format:
    {
        "message": "User's message",
        "system_prompt": "Optional system prompt",
        "model": "gpt-4o",          # or "gpt-4o-mini", "gpt-4-turbo", etc.
        "temperature": 0.7,         # Optional: 0.0 to 2.0
        "max_tokens": 500,          # Optional: max tokens per response
        "max_turns": 10             # Optional: maximum conversation turns
    }

    Resume Input format:
    {
        "message": "User's next message"
    }
    """
    # Get initial configuration from input
    initial_input = ctx.input or {}
    system_prompt = initial_input.get(
        "system_prompt",
        "You are a helpful AI assistant. Be concise and informative.",
    )
    model = initial_input.get("model", "gpt-4o")
    temperature = initial_input.get("temperature", 0.7)
    max_tokens = initial_input.get("max_tokens", 500)
    max_turns = initial_input.get("max_turns", 10)

    # Initialize conversation state
    messages: list[dict[str, str]] = []
    total_tokens = 0

    # Process first turn
    first_message = initial_input.get("message", "")
    if not first_message:
        return {"error": "No message provided in initial input", "execution_id": ctx.execution_id}

    messages, _, tokens_used = await conversation_turn(
        messages,
        first_message,
        system_prompt,
        model,
        temperature,
        max_tokens,
    )
    total_tokens += tokens_used

    # Main conversation loop - pause between turns
    for turn in range(1, max_turns):
        # Pause and wait for next user input
        # When resumed, pause returns the input provided during resume
        # Use a unique label for each pause to allow Flux to differentiate between them
        resume_input = await pause(f"waiting_for_user_input_turn_{turn}")

        # Get next message from resume input
        next_message = resume_input.get("message", "") if resume_input else ""
        if not next_message:
            return {
                "error": "No message provided in resume input",
                "turn_count": len(messages) // 2,
                "execution_id": ctx.execution_id,
                "conversation_history": messages,
                "total_tokens": total_tokens,
            }

        # Process next turn
        messages, _, tokens_used = await conversation_turn(
            messages,
            next_message,
            system_prompt,
            model,
            temperature,
            max_tokens,
        )
        total_tokens += tokens_used

    # Conversation ended - return final result
    return {
        "status": "conversation_ended",
        "reason": f"Maximum turns ({max_turns}) reached",
        "conversation_history": messages,
        "turn_count": len(messages) // 2,
        "total_tokens": total_tokens,
        "model": model,
        "execution_id": ctx.execution_id,
    }


if __name__ == "__main__":  # pragma: no cover
    import json
    import os
    from flux.secret_managers import SecretManager

    # Check if API key is available
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        print("Error: OPENAI_API_KEY not found in environment.")
        print("Set it using: export OPENAI_API_KEY='your-key'")
        exit(1)

    # Set secret for local execution
    secret_manager = SecretManager.current()
    secret_manager.save("OPENAI_API_KEY", api_key)

    # Start conversation about atmospheric physics
    initial_input = {"message": "Why is the sky blue?", "model": "gpt-4o", "max_turns": 3}

    try:
        # Turn 1
        result = conversational_agent_openai.run(initial_input)
        if result.has_failed:
            raise Exception(f"Workflow failed: {result.output}")

        # Turn 2
        result = conversational_agent_openai.resume(
            result.execution_id,
            {"message": "Why does the sky turn red and orange during sunset?"},
        )
        if result.has_failed:
            raise Exception(f"Workflow failed: {result.output}")

        # Turn 3
        result = conversational_agent_openai.resume(
            result.execution_id,
            {"message": "What color would the sky be on Mars?"},
        )
        if result.has_failed:
            raise Exception(f"Workflow failed: {result.output}")

        # Display conversation and token usage
        print(f"Total Tokens: {result.output.get('total_tokens', 0)}")
        print(json.dumps(result.output.get("conversation_history", []), indent=2))

    except Exception as e:
        print(f"Error: {e}")

Last verified against Flux 0.56.0.