Elixir LangChain

repository·main·Indexed 22 days ago

https://github.com/brainlid/langchain

A framework for integrating AI services and self-hosted models into Elixir applications. It enables the creation of data-aware and agentic applications using components and chains, with core orchestration handled by LangChain.Chains.LLMChain. It supports a wide range of providers including OpenAI, Anthropic Claude, Google Gemini, xAI Grok, AWS Bedrock, and local models via Ollama and Bumblebee. Features include custom Elixir function integration via LangChain.Function and agent behavior evaluation using LangChain.Trajectory.

Tokens
24.5K
Snippets
70
Records
89
Agent score
79%

What's inside Elixir LangChain

  1. Supported Chat Models in Elixir LangChain

    main

    Elixir LangChain supports a wide variety of chat models and providers, including:

    • Anthropic Claude: Includes extended thinking support and AWS Bedrock support.
    • AWS Bedrock Mantle: OpenAI-compatible gateway for Bedrock-hosted models.
    • OpenAI: Via Chat Completions API and the newer Responses API (with WebSocket support).
    • Cloudflare Workers AI: Via ChatOpenAI (OpenAI-compatible).
    • xAI Grok: Grok-4, Grok-3-mini, Grok-4 Heavy, etc.
    • Google: Gemini AI models and Vertex AI.
    • DeepSeek: Supports prompt caching.
    • Mistral: Mistral AI models.
    • Perplexity: Perplexity AI models.
    • Ollama: Locally hosted open-source models.
    • Bumblebee: Self-hosted models via Nx (Llama, Mistral, Zephyr).
    • orq.ai: Deployments API.
    • ReqLLM: Multi-provider adapter (Anthropic, OpenAI, Gemini, Grok, Ollama, AWS Bedrock, etc.).
  2. How LLMChain message assembly and event emission works

    main

    LLMChain handles responses through two distinct paths depending on whether streaming is enabled. Understanding the process ownership and event flow is critical for correct implementation.

    Non-Streaming Path (stream: false)

    When streaming is disabled, the chat model makes a single API call and returns a complete Message struct.

    1. LLMChain.run/2 calls the chat model's call/3.
    2. The model returns {:ok, Message}.
    3. :on_llm_new_message callback fires with the complete message.
    4. The chain runs message processors (JSON parsing, validation, etc.).
    5. :on_message_processed callback fires with the final message.
    6. The message is added to the chain's message list.

    Process Context: Runs synchronously in the calling process, though in LiveView/GenServer contexts, it is recommended to run this in an async Task.

    Streaming Path (stream: true)

    When streaming is enabled, responses arrive as partial MessageDelta structs. This involves two separate processes:

    1. HTTP Streaming Context (Req Task):

    • Raw bytes arrive from the LLM API.
    • The chat model decodes bytes into MessageDelta structs.
    • :on_llm_new_delta callback fires immediately for each delta batch.
    • Deltas accumulate in the HTTP response body.

    2. Main Process Context (LLMChain caller):

    • After the stream ends, LLMChain receives the accumulated deltas.
    • apply_deltas/2 merges them using MessageDelta.merge_delta/2.
    • When the delta status becomes :complete, it is converted to a Message.
    • The chain fires :on_message_processed with the completed message.

    Key Insight: The :on_llm_new_delta callback runs in the HTTP task context, not the main process. For LiveView/GenServer integrations, these callbacks must send messages to the main process to update state.

  3. How LangChain components and chains work

    main

    LangChain is a framework for building data-aware and agentic applications. It is built on two primary abstractions:

    1. Components: Modular abstractions for working with language models (and other integrations). These are easy to use individually or as part of the larger framework.
    2. Off-the-shelf chains: Structured assemblies of components designed to accomplish specific high-level tasks.

    Developers can use pre-built chains for quick starts or compose custom chains using individual components for more complex use cases.

  4. Monitor LangChain with Telemetry (:telemetry events)

    main

    LangChain emits native Elixir :telemetry events for monitoring LLM calls, chain runs, and tool calls. This layer requires no extra dependencies and is used for monitoring and tracing application behavior.

    Event Lifecycle Triples

    Events follow the pattern [:langchain, component, operation, stage]. The following lifecycle triples are emitted:

    • [:langchain, :llm, :call, :start | :stop | :exception]
    • [:langchain, :chain, :execute, :start | :stop | :exception]
    • [:langchain, :tool, :call, :start | :stop | :exception]

    Content-bearing events (opt-in) include:

    • [:langchain, :llm, :prompt]
    • [:langchain, :llm, :response]

    Key Metadata and Rules

    • Correlation: Use the :call_id UUID in the metadata to correlate :start, :stop, and :exception events for a single operation.
    • Latency: Read the duration measurement from :stop and :exception events.
    • Token Usage: LLM :stop events and chain :stop events carry a %LangChain.TokenUsage{} struct. For chains, this is aggregated across all assistant messages in the run.
    • Provider: The :provider key (e.g., "openai", "anthropic") is available on LLM call events.
    • Request Options: LLM call events include :request_options, a map of parameters like :temperature, :max_tokens, and :top_p.
    • Chain Metadata: Use the key :tools_count (not :tool_count) to find the number of tools used.
    • Privacy: Lifecycle events (:start/:stop/:exception) never carry message content. Content is only present in the specific :prompt and :response events.
  5. Evaluate agent behavior with LangChain.Trajectory

    main

    When building agent systems, you can use LangChain.Trajectory to evaluate the reasoning process (the sequence of tool calls) rather than just the final answer. A trajectory captures the structured sequence of tool calls produced during an LLMChain run, which is useful for regression testing, cost control, and debugging.

    To capture a trajectory, run your chain and then call Trajectory.from_chain(chain).

    alias LangChain.Trajectory
    
    {:ok, chain} =
      LLMChain.new!(%{llm: llm})
      |> LLMChain.add_tools(my_tools)
      |> LLMChain.add_message(Message.new_user!("What's the weather in Paris?"))
      |> LLMChain.run(mode: :while_needs_response)
    
    trajectory = Trajectory.from_chain(chain)
    trajectory.tool_calls
    #=> [%{name: "search", arguments: %{"query" => "weather paris"}},
    #    %{name: "get_forecast", arguments: %{"city" => "Paris"}}]
  6. Understand the LangChain observability layers

    main

    LangChain provides two independent layers for monitoring LLM applications:

    1. LangChain.Telemetry: A vendor-neutral layer that emits standard :telemetry events for every LLM call, chain execution, and tool call. This has no extra dependencies and can be used with any :telemetry handler (e.g., Logger, PromEx, Telemetry.Metrics).

    2. LangChain.OpenTelemetry: An optional integration that translates telemetry events into OpenTelemetry spans and metrics. This follows a subset of the GenAI Semantic Conventions (v1.40+) and is intended for distributed tracing backends like Langfuse, Honeycomb, Grafana Tempo, or Jaeger.

    Note that the OpenTelemetry layer is built on top of the Telemetry layer; the core events apply to both.

  7. Pre-filling the assistant's response

    main

    Pre-filling an assistant's response (starting an assistant message with a specific prefix to guide the model's output) behaves differently depending on the provider:

    • Anthropic Claude 3: Responds well to pre-filled assistant responses and this pattern is officially encouraged.
    • OpenAI (ChatGPT 3.5 and 4): Does not reliably complete pre-filled responses. For example, if you instruct the model to use an <answer>{{ANSWER}}</answer> template and start the assistant message with <answer>, it may fail to include the closing </answer> tag.
  8. Attach domain context and conversation IDs to spans

    main

    To group traces into conversations or add domain-specific metadata (like user.id), use the :custom_context map on your chain.

    • Conversation ID: Set the :conversation_id key (or :langfuse_session_id) in custom_context to group multi-turn sessions via the gen_ai.conversation.id attribute.
    • Agent Identity: Set :agent_name and :agent_id in custom_context to populate gen_ai.agent.name and gen_ai.agent.id.
    • Custom Attributes: Use the reserved :otel_attributes key in custom_context to attach flat maps of metadata. These attributes are inherited by all child spans (LLM calls, tool calls) produced by the chain.

    Note: Only the :otel_attributes key is exported to spans to prevent leaking large internal structs or PIDs.

    # Grouping by conversation and adding domain context
    chain
    |> Map.put(:custom_context, %{
      conversation_id: "session_123",
      otel_attributes: %{
        "user.id" => "user_abc",
        "organization.id" => "org_xyz"
      }
    })
  9. Configure Agent identity in OpenTelemetry

    main

    To ensure your OpenTelemetry spans correctly identify which agent is running, provide the agent's name and ID in your custom_context.

    If you do not provide these, the gen_ai.agent.name attribute will fall back to the generic chain type (e.g., "llm_chain"), which prevents effective grouping of traces by specific agents.

    # Example of setting agent identity in custom_context
    custom_context = %{
      agent_name: "customer_support_agent",
      agent_id: "agent_123"
    }
  10. Understanding token usage reporting

    main

    Token usage reporting varies across providers:

    • GoogleAI: Returns token usage for each MessageDelta. Note that GoogleAI generates incremental values with each message.
    • OpenAI ChatGPT, Anthropic Claude, and Bumblebee: Return token usage information only at the end of the interaction.
  11. Use LangChain.Chains.LLMChain for core logic

    main

    The central module for interacting with LLMs in this library is LangChain.Chains.LLMChain. Most other components (chat models, messages, tools) are designed to be passed into an LLMChain to orchestrate the conversation flow. To use it, initialize a chain with an LLM, add messages, and call run().

    # Conceptual pattern
    {:ok, chain} = 
      LLMChain.new!(%{llm: chat_model})
      |> LLMChain.add_message(Message.new_user!("Hello"))
      |> LLMChain.run()
  12. How Tool Call `display_text` works during streaming

    main

    When streaming tool calls, the library automatically provides a display_text field for each ToolCall. This allows the UI to show human-friendly labels (e.g., "Reading file" instead of file_read) immediately as the tool is identified.

    Resolution Logic:

    1. If Function.display_text is defined on the tool, it is used.
    2. Otherwise, Utils.humanize_tool_name/1 is used (e.g., "file_read" becomes "File read").

    Streaming Behavior:

    • The :on_llm_new_delta callback is enriched with display_text via rewrap_callbacks_for_model. This ensures the UI can show tool names the moment they appear in the stream.
    • The post-streaming path (apply_deltas) also independently sets display_text as an idempotent guard.