├── .llm-context ├── lc-project-notes.md ├── .gitignore ├── templates │ └── lc │ │ ├── prompt.j2 │ │ ├── files.j2 │ │ ├── end-prompt.j2 │ │ ├── definitions.j2 │ │ ├── outlines.j2 │ │ ├── context.j2 │ │ ├── excluded.j2 │ │ ├── missing-files.j2 │ │ ├── excerpts.j2 │ │ └── overview.j2 ├── rules │ ├── prm-code.md │ ├── lc │ │ ├── flt-no-outline.md │ │ ├── flt-no-full.md │ │ ├── flt-no-files.md │ │ ├── prm-developer.md │ │ ├── prm-rule-create.md │ │ ├── exc-base.md │ │ ├── ins-rule-intro.md │ │ ├── sty-jupyter.md │ │ ├── sty-python.md │ │ ├── sty-javascript.md │ │ ├── ins-developer.md │ │ ├── sty-code.md │ │ ├── flt-base.md │ │ └── ins-rule-framework.md │ ├── flt-repo.md │ └── sty-elixir.md └── config.yaml ├── .env ├── test ├── test_helper.exs └── openai_ex_test.exs ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml └── workflows │ └── ci.yml ├── .vscode └── settings.json ├── assets ├── transcribe.mp3 ├── images │ └── starmask.png ├── fine-tune.jsonl ├── batch-requests.jsonl └── cyberdyne.txt ├── .formatter.exs ├── .devcontainer ├── env ├── devcontainer.json └── docker-compose.yml ├── lib ├── openai_ex │ ├── application.ex │ ├── images_variation.ex │ ├── images_edit.ex │ ├── images_generate.ex │ ├── msg_content.ex │ ├── audio │ │ ├── speech.ex │ │ ├── translation.ex │ │ └── transcription.ex │ ├── moderations.ex │ ├── beta │ │ ├── threads_runs_step.ex │ │ ├── vector_stores_files.ex │ │ ├── vector_stores_file_batches.ex │ │ ├── vector_stores.ex │ │ ├── threads.ex │ │ ├── threads_messages.ex │ │ ├── assistants.ex │ │ └── threads_runs.ex │ ├── models.ex │ ├── embeddings.ex │ ├── images.ex │ ├── vector_stores_files.ex │ ├── completion.ex │ ├── vector_stores_file_batches.ex │ ├── chat_message.ex │ ├── batches.ex │ ├── vector_stores.ex │ ├── conversations.ex │ ├── containers.ex │ ├── conversation_items.ex │ ├── files.ex │ ├── fine_tuning_jobs.ex │ ├── responses.ex │ ├── http.ex │ ├── error.ex │ ├── http_finch.ex │ ├── container_files.ex │ ├── chat_completions.ex │ ├── http_sse.ex │ └── evals.ex └── openai_ex.ex ├── .gitignore ├── notebooks ├── cleanup.livemd ├── images.livemd ├── completions.livemd ├── streaming_orderbot.livemd ├── dlai_orderbot.livemd └── beta_guide.livemd ├── mix.exs ├── CHANGELOG.md ├── mix.lock └── README.md /.llm-context/lc-project-notes.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=openai_ex -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.llm-context/.gitignore: -------------------------------------------------------------------------------- 1 | curr_ctx.yaml 2 | lc-state.yaml -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.defaultProfile.linux": "bash", 3 | } -------------------------------------------------------------------------------- /assets/transcribe.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberchitta/openai_ex/HEAD/assets/transcribe.mp3 -------------------------------------------------------------------------------- /assets/images/starmask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberchitta/openai_ex/HEAD/assets/images/starmask.png -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 120 5 | ] 6 | -------------------------------------------------------------------------------- /.llm-context/templates/lc/prompt.j2: -------------------------------------------------------------------------------- 1 | {%- if prompt -%} 2 | {{ prompt }} 3 | {%- if user_notes %} 4 | 5 | {{ user_notes }} 6 | {%- endif -%} 7 | {%- endif %} -------------------------------------------------------------------------------- /.llm-context/templates/lc/files.j2: -------------------------------------------------------------------------------- 1 | {% for item in files -%} 2 | {{ item.path }} {% if item.path in rule_included_paths %}(Key file){% endif %} 3 | ॥๛॥ 4 | {{ item.content }} 5 | ॥๛॥ 6 | {% endfor %} -------------------------------------------------------------------------------- /.llm-context/templates/lc/end-prompt.j2: -------------------------------------------------------------------------------- 1 | {% if prompt %} 2 | --- 3 | 4 | **The user's request/question/task will follow in the next message. Please respond with "Ready for your request" and wait for the user's input.** 5 | {% endif %} -------------------------------------------------------------------------------- /assets/fine-tune.jsonl: -------------------------------------------------------------------------------- 1 | {"prompt": "", "completion": ""} 2 | {"prompt": "", "completion": ""} 3 | {"prompt": "", "completion": ""} 4 | -------------------------------------------------------------------------------- /.llm-context/rules/prm-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: prm-code 3 | description: default coding rule for this repo. 4 | instructions: [lc/ins-developer, lc/sty-code, sty-elixir] 5 | compose: 6 | filters: [flt-repo] 7 | excerpters: [lc/exc-base] 8 | --- 9 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/flt-no-outline.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Excludes all files from outline selection using gitignore patterns. Use to focus context on full file content without structural summaries. 3 | gitignores: 4 | excerpted-files: ["**/*"] 5 | --- 6 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/flt-no-full.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Excludes all files from full content selection using gitignore patterns. Use to restrict context to code outlines or metadata, minimizing context size. 3 | gitignores: 4 | full-files: ["**/*"] 5 | --- 6 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/flt-no-files.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Excludes all files from both full and outline selections. Use for minimal project contexts that include only metadata or notes, ideal for high-level planning. 3 | compose: 4 | filters: [lc/flt-no-full, lc/flt-no-outline] 5 | --- 6 | -------------------------------------------------------------------------------- /.llm-context/templates/lc/definitions.j2: -------------------------------------------------------------------------------- 1 | {% for item in definitions -%} 2 | {{ item.path }}:{{ item.name }} 3 | ॥๛॥ 4 | {%- for implementation in item.code %} 5 | {{ implementation }} 6 | {%- if not loop.last %} 7 | ⋮... 8 | {% endif -%} 9 | {%- endfor %} 10 | ॥๛॥ 11 | {% endfor %} -------------------------------------------------------------------------------- /.devcontainer/env: -------------------------------------------------------------------------------- 1 | # copy this file to `.env` and set these VARS to the actual vals 2 | OPENAI_API_KEY=pasteYourActualApiKeyHere 3 | OPENAI_ORGANIZATION=pasteYourActualOrgHere 4 | LIVEBOOK_PASSWORD=pasteSuperSecretPasswordHere 5 | # dialyzer write permissions workaround (for macos) 6 | UID=0 7 | GID=0 -------------------------------------------------------------------------------- /.llm-context/rules/lc/prm-developer.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Configures a base prompt for developer workflows, composing standard file filters to include essential code files (e.g., .py, .js, .ts). 3 | instructions: [lc/ins-developer] 4 | compose: 5 | filters: [lc/flt-base] 6 | excerpters: [lc/exc-base] 7 | --- 8 | -------------------------------------------------------------------------------- /.llm-context/templates/lc/outlines.j2: -------------------------------------------------------------------------------- 1 | {% for excerpts_group in excerpts %} 2 | {% if excerpts_group.excerpts and excerpts_group.excerpts[0].metadata.processor_type == "code-outliner" %} 3 | {% for item in excerpts_group.excerpts %} 4 | {{ item.rel_path }} 5 | ॥๛॥ 6 | {{ item.content }} 7 | ॥๛॥ 8 | {% endfor %} 9 | {% endif %} 10 | {% endfor %} -------------------------------------------------------------------------------- /lib/openai_ex/application.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | @impl true 7 | def start(_type, _args) do 8 | children = [ 9 | {Finch, name: OpenaiEx.Finch}, 10 | {DynamicSupervisor, strategy: :one_for_one, name: OpenaiEx.FinchSupervisor} 11 | ] 12 | 13 | Supervisor.start_link(children, strategy: :one_for_one) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenaiEx", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "livebook", 5 | "workspaceFolder": "/data", 6 | "shutdownAction": "stopCompose", 7 | "customizations": { 8 | "vscode": { 9 | "extensions": [ 10 | "jakebecker.elixir-ls", 11 | "eamodio.gitlens" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/prm-rule-create.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Generates a complete project context with instructions for creating focused rules, including new chat prefixes and common guidelines. Includes all rule files in full content for reference. Use for efficient rule creation tasks. 3 | instructions: ["lc/ins-rule-intro", "lc/ins-rule-framework"] 4 | overview: full 5 | compose: 6 | filters: [lc/flt-base] 7 | excerpters: [lc/exc-base] 8 | also-include: 9 | full-files: [/.llm-context/rules/**] 10 | --- 11 | -------------------------------------------------------------------------------- /.llm-context/templates/lc/context.j2: -------------------------------------------------------------------------------- 1 | {% include 'lc/prompt.j2' %} 2 | {%- if rules %} 3 | 4 | {% for item in rules -%} 5 | {{ item.content }} 6 | {% endfor %} 7 | {%- endif -%} 8 | {% if project_notes %} 9 | {{ project_notes }} 10 | {% endif %} 11 | 12 | {% include 'lc/overview.j2' %} 13 | 14 | {% if files %} 15 | ## Complete File Contents 16 | 17 | {% include 'lc/files.j2' %} 18 | {% endif %} 19 | 20 | {% if excerpts %} 21 | {% include 'lc/excerpts.j2' %} 22 | {% endif %} 23 | 24 | {% include 'lc/end-prompt.j2' %} -------------------------------------------------------------------------------- /test/openai_ex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule OpenaiExTest do 2 | use ExUnit.Case 3 | doctest OpenaiEx.Chat.Completions 4 | doctest OpenaiEx.Completion 5 | doctest OpenaiEx.ChatMessage 6 | doctest OpenaiEx.Embeddings 7 | doctest OpenaiEx.Images.Generate 8 | doctest OpenaiEx.Moderations 9 | doctest OpenaiEx.MsgContent 10 | doctest OpenaiEx.Beta.Assistants 11 | doctest OpenaiEx.Beta.Threads.Runs 12 | doctest OpenaiEx.Containers 13 | doctest OpenaiEx.ContainerFiles 14 | doctest OpenaiEx.VectorStores 15 | end 16 | -------------------------------------------------------------------------------- /.llm-context/config.yaml: -------------------------------------------------------------------------------- 1 | __info__: 'This project uses llm-context. For more information, visit: https://github.com/cyberchitta/llm-context.py 2 | or https://pypi.org/project/llm-context/' 3 | templates: 4 | context: lc/context.j2 5 | definitions: lc/definitions.j2 6 | end-prompt: lc/end-prompt.j2 7 | excerpts: lc/excerpts.j2 8 | excluded: lc/excluded.j2 9 | files: lc/files.j2 10 | missing-files: lc/missing-files.j2 11 | outlines: lc/outlines.j2 12 | overview: lc/overview.j2 13 | prompt: lc/prompt.j2 14 | -------------------------------------------------------------------------------- /.llm-context/rules/flt-repo.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: flt-repo 3 | description: additional repo specific filters. 4 | compose: 5 | filters: [lc/flt-base] 6 | gitignores: 7 | full-files: 8 | - beta/ 9 | - notebooks/ 10 | - test/ 11 | - .github/ 12 | - .devcontainer/ 13 | - .formatter.exs 14 | - .vscode/ 15 | - assets/ 16 | - '*.md' 17 | - '*.scm' 18 | - '*.yaml' 19 | excerpted-files: 20 | - beta/ 21 | - notebooks/ 22 | - test/ 23 | - .github/ 24 | - .devcontainer/ 25 | - .formatter.exs 26 | - .vscode/ 27 | - assets/ 28 | - '*.md' 29 | - '*.scm' 30 | - '*.yaml' 31 | --- 32 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | livebook: 3 | image: ghcr.io/livebook-dev/livebook:0.17.1 4 | environment: 5 | # secrets - values are set in '.env'. It's not in version control, but look at 'env' 6 | LIVEBOOK_PASSWORD: ${LIVEBOOK_PASSWORD} 7 | OPENAI_API_KEY: ${OPENAI_API_KEY} 8 | OPENAI_ORGANIZATION: ${OPENAI_ORGANIZATION} 9 | # UID (id -u) and GID (id -g) need to be exported in .bashrc (or equivalent) 10 | user: ${UID}:${GID} 11 | volumes: 12 | - ..:/data 13 | ports: 14 | - 8080:8080 15 | - 8081:8081 16 | # port for llama.cpp-python server to work with local LLMs 17 | - 8000:8000 18 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/exc-base.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Base excerpt mode mappings and default configurations 3 | excerpt-modes: 4 | "*.py": code-outliner 5 | "*.js": code-outliner 6 | "*.ts": code-outliner 7 | "*.jsx": code-outliner 8 | "*.tsx": code-outliner 9 | "*.java": code-outliner 10 | "*.cpp": code-outliner 11 | "*.c": code-outliner 12 | "*.cs": code-outliner 13 | "*.go": code-outliner 14 | "*.rs": code-outliner 15 | "*.rb": code-outliner 16 | "*.php": code-outliner 17 | "*.ex": code-outliner 18 | "*.elm": code-outliner 19 | "*.svelte": sfc 20 | excerpt-config: 21 | sfc: 22 | with-style: false 23 | with-template: false 24 | --- 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this library 3 | labels: ["feature-request"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this feature request! 9 | - type: textarea 10 | id: feature 11 | attributes: 12 | label: Describe the feature or improvement you're requesting 13 | description: A clear and concise description of what you want to happen. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: context 18 | attributes: 19 | label: Additional context 20 | description: Add any other context about the feature request here. -------------------------------------------------------------------------------- /.llm-context/templates/lc/excluded.j2: -------------------------------------------------------------------------------- 1 | {%- if not_excerpted -%} 2 | The following files are not included as excerpted content: 3 | {% for file in not_excerpted -%} 4 | {{ file }} 5 | {% endfor %} 6 | 7 | {% endif -%} 8 | {%- if excluded_content -%} 9 | {% for excluded in excluded_content -%} 10 | {% for section_name, content in excluded.sections.items() -%} 11 | {{ excluded.metadata.file }} ({{ section_name }}) 12 | ॥๛॥ 13 | {{ content }} 14 | ॥๛॥ 15 | {% endfor %} 16 | {% endfor %} 17 | {%- endif -%} 18 | {%- if not requested_excerpted and not not_excerpted -%} 19 | No files requested. 20 | {%- elif not excluded_content and requested_excerpted -%} 21 | No excluded sections found for the requested excerpted files. 22 | {%- endif -%} -------------------------------------------------------------------------------- /lib/openai_ex/images_variation.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Images.Variation do 2 | @moduledoc """ 3 | This module provides constructors for OpenAI Image Variation API request structure. The API reference can be found at https://platform.openai.com/docs/api-reference/images/create-variation. 4 | """ 5 | @api_fields [ 6 | :image, 7 | :model, 8 | :n, 9 | :response_format, 10 | :size, 11 | :user 12 | ] 13 | 14 | @doc """ 15 | Creates a new image variation request 16 | """ 17 | 18 | def new(args = [_ | _]) do 19 | args |> Enum.into(%{}) |> new() 20 | end 21 | 22 | def new(args = %{image: _}) do 23 | args |> Map.take(@api_fields) 24 | end 25 | 26 | @doc false 27 | def file_fields() do 28 | [:image] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /assets/batch-requests.jsonl: -------------------------------------------------------------------------------- 1 | {"custom_id": "req1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is the capital of France?"}]}} 2 | {"custom_id": "req2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Can you explain the concept of machine learning in simple terms?"}]}} 3 | {"custom_id": "req3", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Write a short poem about the beauty of nature."}]}} -------------------------------------------------------------------------------- /lib/openai_ex/images_edit.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Images.Edit do 2 | @moduledoc """ 3 | This module provides constructors for OpenAI Image Edit API request structure. The API reference can be found at https://platform.openai.com/docs/api-reference/images/create-edit. 4 | """ 5 | @api_fields [ 6 | :image, 7 | :prompt, 8 | :mask, 9 | :model, 10 | :n, 11 | :quality, 12 | :response_format, 13 | :size, 14 | :user 15 | ] 16 | 17 | @doc """ 18 | Creates a new image edit request 19 | """ 20 | def new(args = [_ | _]) do 21 | args |> Enum.into(%{}) |> new() 22 | end 23 | 24 | def new(args = %{image: _, prompt: _}) do 25 | args |> Map.take(@api_fields) 26 | end 27 | 28 | @doc false 29 | def file_fields() do 30 | [:image, :mask] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | openai_ex-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # Directory used by the VS Code elixir-ls extension 29 | /.elixir_ls/ 30 | 31 | # secrets used by .devcontainer docker compose 32 | /.devcontainer/.env 33 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/ins-rule-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Introduces the project focus creation guide for new chat sessions, emphasizing minimal file inclusion and multi-project coordination. Use to initiate rule creation in conversational workflows with LLMs. 3 | --- 4 | 5 | # Project Focus Creation Guide 6 | 7 | You have been provided with complete project context to help create focused, task-specific rules that include only the minimum necessary files for efficient LLM conversations. 8 | 9 | ## Your Mission 10 | 11 | Analyze the provided project structure and help the user create a focused rule that includes only the essential files needed for their specific task, dramatically reducing context size while maintaining effectiveness. 12 | 13 | ## Multi-Project Contexts 14 | 15 | When working with multiple projects, you'll need to create separate rules for each project. Coordinate the file selections across projects to ensure the combined context provides what's needed for the task. 16 | -------------------------------------------------------------------------------- /lib/openai_ex/images_generate.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Images.Generate do 2 | @moduledoc """ 3 | This module provides constructors for the OpenAI image generation API. The API 4 | reference can be found at https://platform.openai.com/docs/api-reference/images/create. 5 | """ 6 | @api_fields [ 7 | :prompt, 8 | :background, 9 | :model, 10 | :moderation, 11 | :n, 12 | :output_compression, 13 | :output_format, 14 | :quality, 15 | :response_format, 16 | :size, 17 | :style, 18 | :user 19 | ] 20 | 21 | @doc """ 22 | Creates a new image generation request 23 | 24 | Example usage: 25 | 26 | iex> _request = OpenaiEx.Images.Generate.new(prompt: "This is a test") 27 | %{prompt: "This is a test"} 28 | 29 | iex> _request = OpenaiEx.Images.Generate.new(%{prompt: "This is a test"}) 30 | %{prompt: "This is a test"} 31 | """ 32 | def new(args = [_ | _]) do 33 | args |> Enum.into(%{}) |> new() 34 | end 35 | 36 | def new(args = %{prompt: _}) do 37 | args |> Map.take(@api_fields) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/openai_ex/msg_content.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.MsgContent do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Create text content. 6 | 7 | Example usage: 8 | 9 | iex> _message = OpenaiEx.MsgContent.text("Hello, world!") 10 | %{text: "Hello, world!", type: "text"} 11 | """ 12 | def text(content), do: %{type: "text", text: content} 13 | 14 | @doc """ 15 | Create image content (from file_id) 16 | 17 | Example usage: 18 | 19 | iex> _message = OpenaiEx.MsgContent.image_file("file-BK7bzQj3FfZFXr7DbL6xJwfo") 20 | %{image_file: %{file_id: "file-BK7bzQj3FfZFXr7DbL6xJwfo"}, type: "image_file"} 21 | """ 22 | def image_file(file_id), do: %{type: "image_file", image_file: %{file_id: file_id}} 23 | 24 | @doc """ 25 | Create image content (from url) 26 | 27 | Example usage: 28 | 29 | iex> _message = OpenaiEx.MsgContent.image_url("https://upload.wikimedia.org/fake_image_path.jpg") 30 | %{image_url: "https://upload.wikimedia.org/fake_image_path.jpg", type: "image_url"} 31 | """ 32 | def image_url(url), do: %{type: "image_url", image_url: url} 33 | end 34 | -------------------------------------------------------------------------------- /lib/openai_ex/audio/speech.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Audio.Speech do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI audio speech API. The API reference can be found at https://platform.openai.com/docs/api-reference/audio/createSpeech. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :input, 9 | :model, 10 | :voice, 11 | :response_format, 12 | :speed 13 | ] 14 | 15 | @doc """ 16 | Creates a new audio speech request 17 | """ 18 | def new(args = [_ | _]) do 19 | args |> Enum.into(%{}) |> new() 20 | end 21 | 22 | def new(args = %{input: _, model: _, voice: _}) do 23 | args |> Map.take(@api_fields) 24 | end 25 | 26 | @doc """ 27 | Calls the audio speech endpoint. 28 | 29 | See https://platform.openai.com/docs/api-reference/audio/createSpeech for more information. 30 | """ 31 | def create!(openai = %OpenaiEx{}, audio = %{}) do 32 | openai |> create(audio) |> Http.bang_it!() 33 | end 34 | 35 | def create(openai = %OpenaiEx{}, audio = %{}) do 36 | openai |> Http.post_no_decode("/audio/speech", json: audio |> Map.take(@api_fields)) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /notebooks/cleanup.livemd: -------------------------------------------------------------------------------- 1 | # Cleaning up after testing various API calls 2 | 3 | ```elixir 4 | Mix.install([ 5 | {:openai_ex, "~> 0.9.18"}, 6 | ]) 7 | ``` 8 | 9 | ## Files cleanup 10 | 11 | ```elixir 12 | apikey = System.fetch_env!("LB_OPENAI_API_KEY") 13 | openai = OpenaiEx.new(apikey) 14 | ``` 15 | 16 | ```elixir 17 | all_files = openai |> OpenaiEx.Files.list!() 18 | ``` 19 | 20 | ```elixir 21 | Enum.each(all_files["data"], fn file -> OpenaiEx.Files.delete!(openai, file["id"]) end) 22 | ``` 23 | 24 | ```elixir 25 | OpenaiEx.Files.list!(openai) 26 | ``` 27 | 28 | ## Assistants cleanup 29 | 30 | ```elixir 31 | all_assistants = OpenaiEx.Beta.Assistants.list!(openai) 32 | ``` 33 | 34 | ```elixir 35 | Enum.each(all_assistants["data"], fn a -> OpenaiEx.Beta.Assistants.delete!(openai, a["id"]) end) 36 | ``` 37 | 38 | ```elixir 39 | OpenaiEx.Beta.Assistants.list!(openai) 40 | ``` 41 | 42 | ## Vector Stores cleanup 43 | 44 | ```elixir 45 | (openai |> OpenaiEx.VectorStores.list!())["data"] 46 | |> Enum.each(fn vs -> openai |> OpenaiEx.VectorStores.delete!(vs["id"]) end) 47 | ``` 48 | 49 | ```elixir 50 | openai |> OpenaiEx.VectorStores.list!() 51 | ``` 52 | -------------------------------------------------------------------------------- /.llm-context/templates/lc/missing-files.j2: -------------------------------------------------------------------------------- 1 | {%- if already_included -%} 2 | Already included (full content): 3 | {% for file in already_included -%} 4 | {{ file }} 5 | {% endfor %} 6 | {% endif -%} 7 | {%- if already_excerpted -%} 8 | Already included (excerpted): 9 | {% for file in already_excerpted -%} 10 | {%- set excerpt_meta = excerpted_metadata.get(file, {}) -%} 11 | {%- set processor_type = excerpt_meta.get('processor_type', 'unknown') -%} 12 | {{ file }}{% if processor_type == 'code-outliner' %} (lc-missing -i for details){% else %} (lc-missing -e for excluded){% endif %} 13 | {% endfor %} 14 | {% endif -%} 15 | {%- if deleted_files -%} 16 | Deleted files (no longer exist): 17 | {% for file in deleted_files -%} 18 | {{ file }} 19 | {% endfor %} 20 | {% endif -%} 21 | {%- if files_to_fetch -%} 22 | Updated content: 23 | {% for item in files_to_fetch -%} 24 | {{ item.path }}{% if item.path in modified_files %} (modified){% elif item.path in missing_files %} (new){% endif %} 25 | ॥๛॥ 26 | {{ item.content }} 27 | ॥๛॥ 28 | {% endfor %} 29 | {%- endif -%} 30 | {%- if not already_included and not already_excerpted and not deleted_files and not files_to_fetch -%} 31 | No files to retrieve. 32 | {%- endif -%} -------------------------------------------------------------------------------- /lib/openai_ex/moderations.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Moderations do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI moderation API. The API reference can be found at https://platform.openai.com/docs/api-reference/moderations. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :input, 9 | :model 10 | ] 11 | 12 | @doc """ 13 | Creates a new moderation request with the given arguments. 14 | 15 | Example usage: 16 | 17 | iex> OpenaiEx.Moderations.new(input: "This is a test") 18 | %{ 19 | input: "This is a test" 20 | } 21 | 22 | iex> OpenaiEx.Moderations.new(%{input: "This is a test"}) 23 | %{ 24 | input: "This is a test" 25 | } 26 | """ 27 | def new(args = [_ | _]) do 28 | args |> Enum.into(%{}) |> new() 29 | end 30 | 31 | def new(args = %{input: _}) do 32 | args |> Map.take(@api_fields) 33 | end 34 | 35 | @doc """ 36 | Calls the moderation endpoint. 37 | """ 38 | def create!(openai = %OpenaiEx{}, moderation) do 39 | openai |> create(moderation) |> Http.bang_it!() 40 | end 41 | 42 | def create(openai = %OpenaiEx{}, moderation) do 43 | openai |> Http.post("/moderations", json: moderation |> Map.take(@api_fields)) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/threads_runs_step.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.Threads.Runs.Steps do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI run steps API. The API reference can be found at https://platform.openai.com/docs/api-reference/run-steps. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | defp ep_url(thread_id, run_id, step_id \\ nil) do 8 | "/threads/#{thread_id}/runs/#{run_id}/steps" <> 9 | if is_nil(step_id), do: "", else: "/#{step_id}" 10 | end 11 | 12 | def retrieve!(openai = %OpenaiEx{}, params = %{thread_id: _, run_id: _, step_id: _}) do 13 | openai |> retrieve(params) |> Http.bang_it!() 14 | end 15 | 16 | def retrieve(openai = %OpenaiEx{}, %{thread_id: thread_id, run_id: run_id, step_id: step_id}) do 17 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(thread_id, run_id, step_id)) 18 | end 19 | 20 | def list!(openai = %OpenaiEx{}, params = %{thread_id: _, run_id: _}) do 21 | openai |> list(params) |> Http.bang_it!() 22 | end 23 | 24 | def list(openai = %OpenaiEx{}, params = %{thread_id: thread_id, run_id: run_id}) do 25 | openai 26 | |> OpenaiEx.with_assistants_beta() 27 | |> Http.get(ep_url(thread_id, run_id), params |> Map.take(OpenaiEx.list_query_fields())) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/openai_ex/audio/translation.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Audio.Translation do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI audio translation API. The API reference can be found at https://platform.openai.com/docs/api-reference/audio/createTranslation. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :file, 9 | :model, 10 | :prompt, 11 | :response_format, 12 | :temperature 13 | ] 14 | 15 | @doc """ 16 | Creates a new audio translation request 17 | """ 18 | def new(args = [_ | _]) do 19 | args |> Enum.into(%{}) |> new() 20 | end 21 | 22 | def new(args = %{file: _, model: _}) do 23 | args |> Map.take(@api_fields) 24 | end 25 | 26 | @doc """ 27 | Calls the audio translation endpoint. 28 | 29 | See https://platform.openai.com/docs/api-reference/audio/createTranslation for more information. 30 | """ 31 | def create!(openai = %OpenaiEx{}, audio = %{}) do 32 | openai |> create(audio) |> Http.bang_it!() 33 | end 34 | 35 | def create(openai = %OpenaiEx{}, audio = %{}) do 36 | multipart = audio |> Http.to_multi_part_form_data(file_fields()) 37 | openai |> Http.post("/audio/translations", multipart: multipart) 38 | end 39 | 40 | @doc false 41 | def file_fields() do 42 | [:file] 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/sty-jupyter.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Specifies style guidelines for Jupyter notebooks (.ipynb), focusing on cell structure, documentation, type annotations, AI-assisted development, and output management. Use for Jupyter-based projects to ensure clear, executable notebooks. 3 | --- 4 | 5 | ## Jupyter Notebook Guidelines 6 | 7 | ### Cell Structure 8 | 9 | - One logical concept per cell (single function, data transformation, or analysis step) 10 | - Execute cells independently when possible - avoid hidden dependencies 11 | - Use meaningful cell execution order that tells a clear story 12 | 13 | ### Documentation Pattern 14 | 15 | - Use markdown cells for descriptions, not code comments 16 | - Code cells should contain zero comments - let expressive code speak for itself 17 | - Focus markdown on _why_ and _context_, not _what_ and _how_ 18 | 19 | ### Type Annotations 20 | 21 | - Use `jaxtyping` and similar libraries for concrete, descriptive type signatures 22 | - Specify array shapes, data types, and constraints explicitly 23 | - Examples: 24 | 25 | ```python 26 | from jaxtyping import Float, Int, Array 27 | 28 | def process_features( 29 | data: Float[Array, "batch height width channels"], 30 | labels: Int[Array, "batch"] 31 | ) -> Float[Array, "batch features"]: 32 | ``` 33 | -------------------------------------------------------------------------------- /lib/openai_ex/audio/transcription.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Audio.Transcription do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI audio transcription API. The API reference can be found at https://platform.openai.com/docs/api-reference/audio/createTranscription. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :file, 9 | :model, 10 | :language, 11 | :prompt, 12 | :response_format, 13 | :temperature, 14 | :timestamp_granularities 15 | ] 16 | 17 | @doc """ 18 | Creates a new audio request with the given arguments. 19 | """ 20 | def new(args = [_ | _]) do 21 | args |> Enum.into(%{}) |> new() 22 | end 23 | 24 | def new(args = %{file: _, model: _}) do 25 | args |> Map.take(@api_fields) 26 | end 27 | 28 | @doc """ 29 | Calls the audio transcription endpoint. 30 | 31 | See https://platform.openai.com/docs/api-reference/audio/createTranscription for more information. 32 | """ 33 | def create!(openai = %OpenaiEx{}, audio = %{}) do 34 | openai |> create(audio) |> Http.bang_it!() 35 | end 36 | 37 | def create(openai = %OpenaiEx{}, audio = %{}) do 38 | multipart = audio |> Http.to_multi_part_form_data(file_fields()) 39 | openai |> Http.post("/audio/transcriptions", multipart: multipart) 40 | end 41 | 42 | @doc false 43 | def file_fields() do 44 | [:file] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/openai_ex/models.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Models do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI Models API. Information about these models can be found at https://platform.openai.com/docs/models. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | defp ep_url(model \\ nil) do 8 | "/models" <> if(is_nil(model), do: "", else: "/#{model}") 9 | end 10 | 11 | @doc """ 12 | Lists the available models. 13 | 14 | https://platform.openai.com/docs/api-reference/models/list 15 | """ 16 | def list!(openai = %OpenaiEx{}) do 17 | openai |> list() |> Http.bang_it!() 18 | end 19 | 20 | def list(openai = %OpenaiEx{}) do 21 | openai |> Http.get(ep_url()) 22 | end 23 | 24 | @doc """ 25 | Retrieves a specific model. 26 | 27 | https://platform.openai.com/docs/api-reference/models/retrieve 28 | """ 29 | def retrieve!(openai = %OpenaiEx{}, model) do 30 | openai |> retrieve(model) |> Http.bang_it!() 31 | end 32 | 33 | def retrieve(openai = %OpenaiEx{}, model) do 34 | openai |> Http.get(ep_url(model)) 35 | end 36 | 37 | @doc """ 38 | Deletes a specific model. 39 | 40 | https://platform.openai.com/docs/api-reference/fine-tunes/delete-model 41 | """ 42 | def delete!(openai = %OpenaiEx{}, model) do 43 | openai |> delete(model) |> Http.bang_it!() 44 | end 45 | 46 | def delete(openai = %OpenaiEx{}, model) do 47 | openai |> Http.delete(ep_url(model)) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | test: 9 | runs-on: ubuntu-24.04 10 | env: 11 | MIX_ENV: test 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | # - pair: 17 | # elixir: "1.12" 18 | # otp: "24.3.4.10" 19 | # - pair: 20 | # elixir: "1.13" 21 | # otp: "25.1" 22 | - pair: 23 | elixir: "1.14" 24 | otp: "25.3" 25 | lint: lint 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - uses: erlef/setup-beam@main 30 | with: 31 | otp-version: ${{matrix.pair.otp}} 32 | elixir-version: ${{matrix.pair.elixir}} 33 | version-type: strict 34 | 35 | - uses: actions/cache@v3 36 | with: 37 | path: deps 38 | key: mix-deps-${{ hashFiles('**/mix.lock') }} 39 | 40 | - run: mix deps.get 41 | 42 | - run: mix format --check-formatted 43 | if: ${{ matrix.lint }} 44 | 45 | - run: mix deps.unlock --check-unused 46 | if: ${{ matrix.lint }} 47 | 48 | - run: mix deps.compile 49 | 50 | - run: mix compile --warnings-as-errors 51 | if: ${{ matrix.lint }} 52 | 53 | - run: mix test 54 | if: ${{ ! matrix.lint }} 55 | 56 | - run: mix test --warnings-as-errors 57 | if: ${{ matrix.lint }} -------------------------------------------------------------------------------- /notebooks/images.livemd: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Image Generation Kino App 4 | 5 | ```elixir 6 | Mix.install([ 7 | {:openai_ex, "~> 0.9.18"}, 8 | {:kino, "~> 0.17.0"} 9 | ]) 10 | 11 | alias OpenaiEx 12 | alias OpenaiEx.Images 13 | ``` 14 | 15 | ## Simple Kino UI 16 | 17 | ```elixir 18 | openai = System.fetch_env!("LB_OPENAI_API_KEY") |> OpenaiEx.new() 19 | ``` 20 | 21 | set default parameters for image generation, and define a function to fetch generated images from a URL. 22 | 23 | ```elixir 24 | size = "256x256" 25 | n = 4 26 | 27 | fetch_blob = fn url -> 28 | Finch.build(:get, url) |> Finch.request!(OpenaiEx.Finch) |> Map.get(:body) 29 | end 30 | ``` 31 | 32 | ### Prompt / Response UI 33 | 34 | ```elixir 35 | text_input = Kino.Input.textarea("Describe Image") 36 | 37 | form = Kino.Control.form([text: text_input], submit: "Generate") 38 | frame = Kino.Frame.new() 39 | 40 | Kino.listen(form, fn %{data: %{text: prompt}} -> 41 | Kino.Frame.render(frame, Kino.Text.new("Running...")) 42 | 43 | res_urls = 44 | openai 45 | |> Images.generate!(%{ 46 | prompt: prompt, 47 | n: n, 48 | size: size 49 | }) 50 | # |> IO.inspect() 51 | |> Map.get("data") 52 | |> Enum.map(fn x -> x["url"] end) 53 | 54 | res_urls 55 | |> Enum.map(fn x -> x |> fetch_blob.() |> Kino.Image.new("image/png") end) 56 | |> Kino.Layout.grid(columns: 2) 57 | |> then(&Kino.Frame.render(frame, &1)) 58 | end) 59 | 60 | Kino.Layout.grid([form, frame], boxed: true, gap: 16) 61 | ``` 62 | -------------------------------------------------------------------------------- /lib/openai_ex/embeddings.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Embeddings do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI embeddings API. The API reference can be found at https://platform.openai.com/docs/api-reference/embeddings. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :input, 9 | :model, 10 | :dimensions, 11 | :encoding_format, 12 | :user 13 | ] 14 | 15 | @doc """ 16 | Creates a new embedding request 17 | 18 | Example usage: 19 | 20 | iex> _request = OpenaiEx.Embeddings.new(model: "text-embedding-ada-002", input: "This is a test") 21 | %{input: "This is a test", model: "text-embedding-ada-002"} 22 | 23 | iex> _request = OpenaiEx.Embeddings.new(%{model: "text-embedding-ada-002", input: "This is a test"}) 24 | %{input: "This is a test", model: "text-embedding-ada-002"} 25 | """ 26 | 27 | def new(args = [_ | _]) do 28 | args |> Enum.into(%{}) |> new() 29 | end 30 | 31 | def new(args = %{model: _, input: _}) do 32 | args |> Map.take(@api_fields) 33 | end 34 | 35 | @ep_url "/embeddings" 36 | 37 | @doc """ 38 | Calls the embedding endpoint. 39 | 40 | See https://platform.openai.com/docs/api-reference/embeddings/create for more information. 41 | """ 42 | def create!(openai = %OpenaiEx{}, embedding = %{}) do 43 | openai |> create(embedding) |> Http.bang_it!() 44 | end 45 | 46 | def create(openai = %OpenaiEx{}, embedding = %{}) do 47 | ep = Map.get(openai, :_ep_path_mapping).(@ep_url) 48 | openai |> Http.post(ep, json: embedding) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: what-happened 11 | attributes: 12 | label: Describe the bug 13 | description: A clear and concise description of what the bug is, and any additional context. 14 | placeholder: Tell us what you see! 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: repro-steps 19 | attributes: 20 | label: To Reproduce 21 | description: Steps to reproduce the behavior. 22 | placeholder: | 23 | 1. Fetch a '...' 24 | 2. Update the '....' 25 | 3. See error 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: code-snippets 30 | attributes: 31 | label: Code snippets 32 | description: If applicable, add code snippets to help explain your problem. 33 | render: JavaScript 34 | validations: 35 | required: false 36 | - type: input 37 | id: os 38 | attributes: 39 | label: OS 40 | placeholder: macOS 41 | validations: 42 | required: true 43 | - type: input 44 | id: language-version 45 | attributes: 46 | label: Elixir version 47 | placeholder: Elixir 1.14.2 48 | validations: 49 | required: true 50 | - type: input 51 | id: lib-version 52 | attributes: 53 | label: Library version 54 | placeholder: openai_ex 0.9.18 55 | validations: 56 | required: true -------------------------------------------------------------------------------- /.llm-context/rules/lc/sty-python.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Provides Python-specific style guidelines, including Pythonic patterns, type system usage, class design, import organization, and idioms. Use for Python projects to ensure consistent, readable, and maintainable code. 3 | --- 4 | 5 | ## Python-Specific Guidelines 6 | 7 | ### Pythonic Patterns 8 | 9 | - Use list/dict comprehensions over traditional loops 10 | - Leverage tuple unpacking and multiple assignment 11 | - Use conditional expressions for simple conditional logic 12 | - Prefer single-pass operations: `sum(x for x in items if condition)` over separate filter+sum 13 | 14 | ### Type System 15 | 16 | - Use comprehensive type hints throughout 17 | - Import types from `typing` module as needed 18 | - Use specific types: `list[str]` not `list`, `dict[str, int]` not `dict` 19 | 20 | ### Class Design 21 | 22 | - Use `@dataclass(frozen=True)` as the default for all classes 23 | - Keep `__init__` methods trivial - delegate complex construction to `@staticmethod create()` methods 24 | - Design for immutability to enable functional composition 25 | - Use `@property` for computed attributes 26 | 27 | ### Import Organization 28 | 29 | - Always place imports at module top 30 | - Never use function-level imports except for documented lazy-loading scenarios 31 | - Import order: standard library, third-party, local modules 32 | - Follow PEP 8 naming conventions (snake_case for functions/variables, PascalCase for classes) 33 | 34 | ### Python Idioms 35 | 36 | - Use `isinstance()` for type checking 37 | - Leverage `enumerate()` and `zip()` for iteration 38 | - Use context managers (`with` statements) for resource management 39 | - Prefer `pathlib.Path` over string path manipulation 40 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/sty-javascript.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Details JavaScript-specific style guidelines, covering modern features, module systems, object design, asynchronous code, naming conventions, and documentation. Use for JavaScript projects to ensure consistent code style. 3 | --- 4 | 5 | ## JavaScript-Specific Guidelines 6 | 7 | ### Modern JavaScript Features 8 | 9 | - Use array methods (`map`, `filter`, `reduce`) over traditional loops 10 | - Leverage arrow functions for concise expressions 11 | - Use destructuring assignment for objects and arrays 12 | - Prefer template literals over string concatenation 13 | - Use spread syntax (`...`) for array/object operations 14 | 15 | ### Module System 16 | 17 | - Prefer named exports over default exports (better tree-shaking and refactoring) 18 | - Use consistent import/export patterns 19 | - Structure modules with clear, focused responsibilities 20 | 21 | ### Object Design 22 | 23 | - Use `Object.freeze()` to enforce immutability 24 | - Keep constructors simple - use static factory methods for complex creation 25 | - Use class syntax for object-oriented patterns 26 | - Prefer composition through mixins or utility functions 27 | 28 | ### Asynchronous Code 29 | 30 | - Use `async/await` over Promise chains for better readability 31 | - Handle errors with proper try/catch blocks 32 | - Error messages must include: what failed, why it failed, and suggested action 33 | 34 | ### Naming Conventions 35 | 36 | - Use kebab-case for file names 37 | - Use PascalCase for classes and constructors 38 | - Use camelCase for functions, variables, and methods 39 | - Use UPPER_SNAKE_CASE for constants 40 | 41 | ### Documentation 42 | 43 | - Use JSDoc comments for public APIs and complex business logic 44 | - Document parameter and return types with JSDoc tags 45 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/ins-developer.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Defines the guidelines for coding tasks. It is typically the beginning of the prompt. 3 | --- 4 | 5 | ## Persona 6 | 7 | Senior developer with 40 years experience. 8 | 9 | ## Guidelines 10 | 11 | 1. Assume questions and code snippets relate to this project unless stated otherwise 12 | 2. Follow project's structure, standards and stack 13 | 3. Provide step-by-step guidance for changes 14 | 4. Explain rationale when asked 15 | 5. Be direct and concise 16 | 6. Think step by step 17 | 7. Use conventional commit format with co-author attribution 18 | 8. Follow project-specific instructions 19 | 20 | ## Response Structure 21 | 22 | 1. Direct answer/solution 23 | 2. Give very brief explanation of approach (only if needed) 24 | 3. Minimal code snippets during discussion phase (do not generate full files) 25 | 26 | ## Code Modification Guidelines 27 | 28 | - **Do not generate complete code implementations until the user explicitly agrees to the approach** 29 | - Discuss the approach before providing complete implementation. Be brief, no need to explain the obvious. 30 | - Consider the existing project structure when suggesting new features 31 | - For significant changes, propose a step-by-step implementation plan before writing extensive code 32 | 33 | ## Commit Message Format 34 | 35 | When providing commit messages, use only a single-line conventional commit title with yourself as co-author unless additional detail is specifically requested: 36 | 37 | ``` 38 | 39 | 40 | Co-authored-by: 41 | ``` 42 | 43 | Example format: Claude 4.5 Sonnet 44 | 45 | (Note: Use your actual model name and identifier, not this example. However the domain part identifies the tool, in this case 'llm-context'.) 46 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.9.18" 5 | @description "Community maintained Elixir library for OpenAI API" 6 | @source_url "https://github.com/cyberchitta/openai_ex" 7 | 8 | def project do 9 | [ 10 | app: :openai_ex, 11 | version: @version, 12 | description: @description, 13 | elixir: "~> 1.12", 14 | elixirc_options: [debug_info: true], 15 | start_permanent: Mix.env() == :prod, 16 | deps: deps(), 17 | package: package(), 18 | docs: docs(), 19 | preferred_cli_env: [ 20 | docs: :docs, 21 | "hex.publish": :docs 22 | ] 23 | ] 24 | end 25 | 26 | def application do 27 | [ 28 | mod: {OpenaiEx.Application, []} 29 | ] 30 | end 31 | 32 | defp deps do 33 | [ 34 | {:finch, "~> 0.20"}, 35 | {:jason, "~> 1.4"}, 36 | {:multipart, "~> 0.4"}, 37 | {:ex_doc, ">= 0.0.0", only: :docs}, 38 | {:credo, "~> 1.7", only: :dev, runtime: false}, 39 | {:dialyxir, "~> 1.4", only: :dev, runtime: false} 40 | ] 41 | end 42 | 43 | defp package do 44 | [ 45 | description: @description, 46 | licenses: ["Apache-2.0"], 47 | links: %{ 48 | "GitHub" => @source_url, 49 | "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md" 50 | } 51 | ] 52 | end 53 | 54 | defp docs do 55 | [ 56 | main: "userguide", 57 | source_url: @source_url, 58 | source_ref: "v#{@version}", 59 | api_reference: false, 60 | extra_section: "Livebooks", 61 | extras: [ 62 | "notebooks/userguide.livemd", 63 | "notebooks/beta_guide.livemd", 64 | "notebooks/streaming_orderbot.livemd", 65 | "notebooks/dlai_orderbot.livemd", 66 | "notebooks/images.livemd", 67 | "notebooks/completions.livemd" 68 | ] 69 | ] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/openai_ex/images.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Images do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI images API. The API reference can be found at https://platform.openai.com/docs/api-reference/images. 4 | """ 5 | alias OpenaiEx.{Images, Http} 6 | 7 | @doc """ 8 | Calls the image generation endpoint. 9 | 10 | See the [OpenAI API Create Image reference](https://platform.openai.com/docs/api-reference/images/create) for more information. 11 | """ 12 | def generate!(openai = %OpenaiEx{}, image = %{}) do 13 | openai |> generate(image) |> Http.bang_it!() 14 | end 15 | 16 | def generate(openai = %OpenaiEx{}, image = %{}) do 17 | openai |> Http.post("/images/generations", json: image) 18 | end 19 | 20 | @doc """ 21 | Calls the image edit endpoint. 22 | 23 | See the [OpenAI API Create Image Edit reference](https://platform.openai.com/docs/api-reference/images/create-edit) for more information. 24 | """ 25 | def edit!(openai = %OpenaiEx{}, image_edit = %{}) do 26 | openai |> edit(image_edit) |> Http.bang_it!() 27 | end 28 | 29 | def edit(openai = %OpenaiEx{}, image_edit = %{}) do 30 | multipart = image_edit |> Http.to_multi_part_form_data(Images.Edit.file_fields()) 31 | openai |> Http.post("/images/edits", multipart: multipart) 32 | end 33 | 34 | @doc """ 35 | Calls the image variation endpoint. 36 | 37 | See the [OpenAI API Create Image Variation reference](https://platform.openai.com/docs/api-reference/images/create-variation) for more information. 38 | """ 39 | def create_variation!(openai = %OpenaiEx{}, image_variation = %{}) do 40 | openai |> create_variation(image_variation) |> Http.bang_it!() 41 | end 42 | 43 | def create_variation(openai = %OpenaiEx{}, image_variation = %{}) do 44 | multipart = image_variation |> Http.to_multi_part_form_data(Images.Variation.file_fields()) 45 | openai |> Http.post("/images/variations", multipart: multipart) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/openai_ex/vector_stores_files.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.VectorStores.Files do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI vector_store files API. The API reference can be found at https://platform.openai.com/docs/api-reference/vector-stores-files. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | defp ep_url(vector_store_id, file_id \\ nil) do 8 | "/vector_stores" <> 9 | if(is_nil(vector_store_id), do: "", else: "/#{vector_store_id}") <> 10 | "/files" <> 11 | if(is_nil(file_id), do: "", else: "/#{file_id}") 12 | end 13 | 14 | def list!(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 15 | openai |> list(vector_store_id, params) |> Http.bang_it!() 16 | end 17 | 18 | def list(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 19 | qry_params = params |> Map.take([:filter | OpenaiEx.list_query_fields()]) 20 | openai |> Http.get(ep_url(vector_store_id), qry_params) 21 | end 22 | 23 | def create!(openai = %OpenaiEx{}, vector_store_id, file_id, params \\ %{}) do 24 | openai |> create(vector_store_id, file_id, params) |> Http.bang_it!() 25 | end 26 | 27 | def create(openai = %OpenaiEx{}, vector_store_id, file_id, params \\ %{}) do 28 | json = 29 | %{file_id: file_id} 30 | |> Map.merge(params |> Map.take([:attributes])) 31 | 32 | openai |> Http.post(ep_url(vector_store_id), json: json) 33 | end 34 | 35 | def retrieve!(openai = %OpenaiEx{}, vector_store_id, file_id) do 36 | openai |> retrieve(vector_store_id, file_id) |> Http.bang_it!() 37 | end 38 | 39 | def retrieve(openai = %OpenaiEx{}, vector_store_id, file_id) do 40 | openai |> Http.get(ep_url(vector_store_id, file_id)) 41 | end 42 | 43 | def delete!(openai = %OpenaiEx{}, vector_store_id, file_id) do 44 | openai |> delete(vector_store_id, file_id) |> Http.bang_it!() 45 | end 46 | 47 | def delete(openai = %OpenaiEx{}, vector_store_id, file_id) do 48 | openai |> Http.delete(ep_url(vector_store_id, file_id)) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /.llm-context/templates/lc/excerpts.j2: -------------------------------------------------------------------------------- 1 | {% for excerpts_group in excerpts %} 2 | {% if excerpts_group.excerpts %} 3 | {% set processor_type = excerpts_group.excerpts[0].metadata.processor_type %} 4 | 5 | {% if processor_type == "code-outliner" %} 6 | ## Code Outlines - Implementation Retrieval 7 | 8 | Smart outlines highlighting important definitions in the codebase. These show function/class signatures while omitting implementation details to reduce token usage. 9 | 10 | {% set sample_definitions = excerpts_group.metadata.sample_definitions %} 11 | {% if sample_definitions %} 12 | **Get specific implementations:** 13 | {% if tools_available %} 14 | ```json 15 | { 16 | "root_path": "{{ abs_root_path }}", 17 | "param_type": "i", 18 | "data": {{ sample_definitions | tojson }}, 19 | "timestamp": {{ context_timestamp }} 20 | } 21 | ``` 22 | {% else %} 23 | ```bash 24 | lc-missing -i {{ sample_definitions | tojson | tojson }} -t {{ context_timestamp }} 25 | ``` 26 | {% endif %} 27 | {% endif %} 28 | 29 | {% for item in excerpts_group.excerpts %} 30 | {{ item.rel_path }} 31 | ॥๛॥ 32 | {{ item.content }} 33 | ॥๛॥ 34 | {% endfor %} 35 | 36 | {% elif processor_type == "sfc-excerpter" %} 37 | ## Content Excerpts - Key Sections 38 | 39 | Excerpted content showing important sections from files. These sections are configurable per file type: 40 | 41 | **Get excluded sections:** 42 | {% set sample_sfc_file = excerpts_group.excerpts | first %} 43 | {% if sample_sfc_file %} 44 | {% if tools_available %} 45 | ```json 46 | { 47 | "root_path": "{{ abs_root_path }}", 48 | "param_type": "e", 49 | "data": ["{{ sample_sfc_file.rel_path }}"], 50 | "timestamp": {{ context_timestamp }} 51 | } 52 | ``` 53 | {% else %} 54 | ```bash 55 | lc-missing -e "[\"{{ sample_sfc_file.rel_path }}\"]" -t {{ context_timestamp }} 56 | ``` 57 | {% endif %} 58 | {% endif %} 59 | 60 | {% for item in excerpts_group.excerpts %} 61 | {{ item.rel_path }} 62 | ॥๛॥ 63 | {{ item.content }} 64 | ॥๛॥ 65 | {% endfor %} 66 | {% endif %} 67 | {% endif %} 68 | {% endfor %} 69 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/vector_stores_files.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.VectorStores.Files do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI vector_store files API. The API reference can be found at https://platform.openai.com/docs/api-reference/vector-stores-files. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | defp ep_url(vector_store_id, file_id \\ nil) do 8 | "/vector_stores" <> 9 | if(is_nil(vector_store_id), do: "", else: "/#{vector_store_id}") <> 10 | "/files" <> 11 | if(is_nil(file_id), do: "", else: "/#{file_id}") 12 | end 13 | 14 | def list!(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 15 | openai |> list(vector_store_id, params) |> Http.bang_it!() 16 | end 17 | 18 | def list(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 19 | qry_params = params |> Map.take([:filter | OpenaiEx.list_query_fields()]) 20 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(vector_store_id), qry_params) 21 | end 22 | 23 | def create!(openai = %OpenaiEx{}, vector_store_id, file_id) do 24 | openai |> create(vector_store_id, file_id) |> Http.bang_it!() 25 | end 26 | 27 | def create(openai = %OpenaiEx{}, vector_store_id, file_id) do 28 | json = %{file_id: file_id} 29 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(vector_store_id), json: json) 30 | end 31 | 32 | def retrieve!(openai = %OpenaiEx{}, vector_store_id, file_id) do 33 | openai |> retrieve(vector_store_id, file_id) |> Http.bang_it!() 34 | end 35 | 36 | def retrieve(openai = %OpenaiEx{}, vector_store_id, file_id) do 37 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(vector_store_id, file_id)) 38 | end 39 | 40 | def delete!(openai = %OpenaiEx{}, vector_store_id, file_id) do 41 | openai |> delete(vector_store_id, file_id) |> Http.bang_it!() 42 | end 43 | 44 | def delete(openai = %OpenaiEx{}, vector_store_id, file_id) do 45 | openai |> OpenaiEx.with_assistants_beta() |> Http.delete(ep_url(vector_store_id, file_id)) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/openai_ex/completion.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Completion do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI completions API. The API reference can be found at https://platform.openai.com/docs/api-reference/completions. 4 | """ 5 | alias OpenaiEx.{Http, HttpSse} 6 | 7 | @api_fields [ 8 | :model, 9 | :prompt, 10 | :best_of, 11 | :echo, 12 | :frequency_penalty, 13 | :logit_bias, 14 | :logprobs, 15 | :max_tokens, 16 | :n, 17 | :presence_penalty, 18 | :stop, 19 | :suffix, 20 | :temperature, 21 | :top_p, 22 | :user 23 | ] 24 | 25 | @doc """ 26 | Creates a new completion request with the given arguments. 27 | 28 | Example usage: 29 | iex> _request = OpenaiEx.Completion.new(model: "davinci") 30 | %{model: "davinci"} 31 | iex> _request = OpenaiEx.Completion.new(%{model: "davinci"}) 32 | %{model: "davinci"} 33 | """ 34 | 35 | def new(args = [_ | _]) do 36 | args |> Enum.into(%{}) |> new() 37 | end 38 | 39 | def new(args = %{model: _}) do 40 | args |> Map.take(@api_fields) 41 | end 42 | 43 | @ep_url "/completions" 44 | 45 | @doc """ 46 | Calls the completion 'create' endpoint. 47 | 48 | See https://platform.openai.com/docs/api-reference/completions/create for more information. 49 | """ 50 | def create!(openai = %OpenaiEx{}, completion = %{}, stream: true) do 51 | openai |> create(completion, stream: true) |> Http.bang_it!() 52 | end 53 | 54 | def create(openai = %OpenaiEx{}, completion = %{}, stream: true) do 55 | ep = Map.get(openai, :_ep_path_mapping).(@ep_url) 56 | 57 | openai 58 | |> HttpSse.post(ep, json: completion |> Map.take(@api_fields) |> Map.put(:stream, true)) 59 | end 60 | 61 | def create!(openai = %OpenaiEx{}, completion = %{}) do 62 | openai |> create(completion) |> Http.bang_it!() 63 | end 64 | 65 | def create(openai = %OpenaiEx{}, completion = %{}) do 66 | ep = Map.get(openai, :_ep_path_mapping).(@ep_url) 67 | openai |> Http.post(ep, json: completion |> Map.take(@api_fields)) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/openai_ex/vector_stores_file_batches.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.VectorStores.File.Batches do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI vector_store file batches API. The API reference can be found at https://platform.openai.com/docs/api-reference/vector-stores-file-batches. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | defp ep_url(vector_store_id, batch_id \\ nil, action \\ nil) do 8 | "/vector_stores" <> 9 | if(is_nil(vector_store_id), do: "", else: "/#{vector_store_id}") <> 10 | "/file_batches" <> 11 | if(is_nil(batch_id), do: "", else: "/#{batch_id}") <> 12 | if(is_nil(action), do: "", else: "/#{action}") 13 | end 14 | 15 | def list!(openai = %OpenaiEx{}, vector_store_id, batch_id, params \\ %{}) do 16 | openai |> list(vector_store_id, batch_id, params) |> Http.bang_it!() 17 | end 18 | 19 | def list(openai = %OpenaiEx{}, vector_store_id, batch_id, params \\ %{}) do 20 | url = ep_url(vector_store_id, batch_id, "files") 21 | qry_params = params |> Map.take([:filter | OpenaiEx.list_query_fields()]) 22 | openai |> Http.get(url, qry_params) 23 | end 24 | 25 | def create!(openai = %OpenaiEx{}, vector_store_id, file_ids) do 26 | openai |> create(vector_store_id, file_ids) |> Http.bang_it!() 27 | end 28 | 29 | def create(openai = %OpenaiEx{}, vector_store_id, file_ids) do 30 | url = ep_url(vector_store_id) 31 | openai |> Http.post(url, json: %{file_ids: file_ids}) 32 | end 33 | 34 | def retrieve!(openai = %OpenaiEx{}, vector_store_id, batch_id) do 35 | openai |> retrieve(vector_store_id, batch_id) |> Http.bang_it!() 36 | end 37 | 38 | def retrieve(openai = %OpenaiEx{}, vector_store_id, batch_id) do 39 | openai |> Http.get(ep_url(vector_store_id, batch_id)) 40 | end 41 | 42 | def cancel!(openai = %OpenaiEx{}, vector_store_id, batch_id) do 43 | openai |> cancel(vector_store_id, batch_id) |> Http.bang_it!() 44 | end 45 | 46 | def cancel(openai = %OpenaiEx{}, vector_store_id, batch_id) do 47 | url = ep_url(vector_store_id, batch_id, "cancel") 48 | openai |> Http.post(url) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /assets/cyberdyne.txt: -------------------------------------------------------------------------------- 1 | Cyberdyne Systems Employee Handbook 2 | Welcome to Our Team 3 | Welcome to Cyberdyne Systems! We're excited to have you as part of our innovative team. This handbook introduces you to our culture, values, and essential policies. 4 | 5 | 1. Employment Basics 6 | Employment Type: All new employees undergo a 90-day probationary period. Employment is at-will. 7 | Equal Opportunity: Cyberdyne Systems is committed to a workplace free of discrimination and harassment. 8 | 2. Code of Conduct 9 | Integrity and Ethics: Employees must conduct business lawfully and ethically. 10 | Confidentiality: Protecting company and client information is paramount. 11 | 3. Work Hours and Leave 12 | Standard Work Hours: 9:00 AM to 5:00 PM, Monday to Friday. Flex-time and remote work options may be available. 13 | Vacation: 10 paid vacation days per year. 14 | Sick Leave: Up to 5 paid sick days per year. 15 | 4. Compensation and Benefits 16 | Salaries: Reviewed annually. Direct deposit on the last business day of each month. 17 | Benefits: Health insurance, dental, and vision coverage available after 60 days of employment. 18 | 5. Health and Safety 19 | Workplace Safety: Adhere to safety guidelines and report hazards. 20 | Emergency Procedures: Familiarize yourself with emergency exits and procedures. 21 | 6. Technology Use 22 | Company Equipment: Use responsibly and for company business only. 23 | Internet and Email: No inappropriate use. Maintain cybersecurity awareness. 24 | 7. Social Media Policy 25 | Professionalism: Ensure your social media conduct is respectful and doesn’t harm Cyberdyne Systems' image. 26 | 8. Performance Reviews 27 | Annual Reviews: Performance evaluations conducted yearly to discuss achievements and areas for growth. 28 | 9. Disciplinary Policy 29 | Misconduct: Violations may result in disciplinary action, up to and including termination. 30 | Acknowledgment 31 | I acknowledge that I have received and read the Cyberdyne Systems Employee Handbook. I understand the policies and agree to adhere to them. 32 | 33 | Employee Signature: ___________________ 34 | Date: _________________ 35 | 36 | This handbook is a guide and does not constitute an employment contract. Cyberdyne Systems reserves the right to modify any policies. 37 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/vector_stores_file_batches.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.VectorStores.File.Batches do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI vector_store file batches API. The API reference can be found at https://platform.openai.com/docs/api-reference/vector-stores-file-batches. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | defp ep_url(vector_store_id, batch_id \\ nil, action \\ nil) do 8 | "/vector_stores" <> 9 | if(is_nil(vector_store_id), do: "", else: "/#{vector_store_id}") <> 10 | "/file_batches" <> 11 | if(is_nil(batch_id), do: "", else: "/#{batch_id}") <> 12 | if(is_nil(action), do: "", else: "/#{action}") 13 | end 14 | 15 | def list!(openai = %OpenaiEx{}, vector_store_id, batch_id, params \\ %{}) do 16 | openai |> list(vector_store_id, batch_id, params) |> Http.bang_it!() 17 | end 18 | 19 | def list(openai = %OpenaiEx{}, vector_store_id, batch_id, params \\ %{}) do 20 | url = ep_url(vector_store_id, batch_id, "files") 21 | qry_params = params |> Map.take([:filter | OpenaiEx.list_query_fields()]) 22 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(url, qry_params) 23 | end 24 | 25 | def create!(openai = %OpenaiEx{}, vector_store_id, file_ids) do 26 | openai |> create(vector_store_id, file_ids) |> Http.bang_it!() 27 | end 28 | 29 | def create(openai = %OpenaiEx{}, vector_store_id, file_ids) do 30 | url = ep_url(vector_store_id) 31 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(url, json: %{file_ids: file_ids}) 32 | end 33 | 34 | def retrieve!(openai = %OpenaiEx{}, vector_store_id, batch_id) do 35 | openai |> retrieve(vector_store_id, batch_id) |> Http.bang_it!() 36 | end 37 | 38 | def retrieve(openai = %OpenaiEx{}, vector_store_id, batch_id) do 39 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(vector_store_id, batch_id)) 40 | end 41 | 42 | def cancel!(openai = %OpenaiEx{}, vector_store_id, batch_id) do 43 | openai |> cancel(vector_store_id, batch_id) |> Http.bang_it!() 44 | end 45 | 46 | def cancel(openai = %OpenaiEx{}, vector_store_id, batch_id) do 47 | url = ep_url(vector_store_id, batch_id, "cancel") 48 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(url) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/openai_ex/chat_message.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.ChatMessage do 2 | @moduledoc """ 3 | This module provides an elixir map wrapper around the OpenAI message JSON 4 | object which is used in the chat completions and assistants APIs. 5 | """ 6 | @map_fields [ 7 | :content, 8 | :role, 9 | :name, 10 | :tool_call_id 11 | ] 12 | 13 | defp new(args = [_ | _]), do: args |> Enum.into(%{}) |> new() 14 | 15 | defp new(params = %{}) do 16 | params 17 | |> Map.take(@map_fields) 18 | |> Enum.filter(fn {_, v} -> !is_nil(v) end) 19 | |> Enum.into(%{}) 20 | end 21 | 22 | @doc """ 23 | Create a `ChatMessage` map with role `system`. 24 | 25 | Example usage: 26 | 27 | iex> _message = OpenaiEx.ChatMessage.system("Hello, world!") 28 | %{content: "Hello, world!", role: "system"} 29 | """ 30 | def system(content), do: new(role: "system", content: content) 31 | 32 | @doc """ 33 | Create a `ChatMessage` map with role `developer`. 34 | 35 | Example usage: 36 | 37 | iex> _message = OpenaiEx.ChatMessage.developer("Hello, world!") 38 | %{content: "Hello, world!", role: "developer"} 39 | """ 40 | def developer(content), do: new(role: "developer", content: content) 41 | 42 | @doc """ 43 | Create a `ChatMessage` map with role `user`. 44 | 45 | Example usage: 46 | 47 | iex> _message = OpenaiEx.ChatMessage.user("Hello, world!") 48 | %{content: "Hello, world!", role: "user"} 49 | """ 50 | def user(content), do: new(role: "user", content: content) 51 | 52 | @doc """ 53 | Create a `ChatMessage` map with role `assistant`. 54 | 55 | Example usage: 56 | 57 | iex> _message = OpenaiEx.ChatMessage.assistant("Hello, world!") 58 | %{content: "Hello, world!", role: "assistant"} 59 | """ 60 | def assistant(content), do: new(role: "assistant", content: content) 61 | 62 | @doc """ 63 | Create a `ChatMessage` map with role `function`. 64 | 65 | Example usage: 66 | 67 | iex> _message = OpenaiEx.ChatMessage.tool("call_sjflkje", "greet", "Hello, world!") 68 | %{content: "Hello, world!", role: "tool", name: "greet", tool_call_id: "call_sjflkje"} 69 | """ 70 | def tool(tool_call_id, name, content), 71 | do: new(role: "tool", tool_call_id: tool_call_id, content: content, name: name) 72 | end 73 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/vector_stores.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.VectorStores do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI vector_stores API. The API reference can be found at https://platform.openai.com/docs/api-reference/vector-stores. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :file_ids, 9 | :name, 10 | :expires_after, 11 | :metadata 12 | ] 13 | 14 | defp ep_url(vector_store_id \\ nil) do 15 | "/vector_stores" <> 16 | if(is_nil(vector_store_id), do: "", else: "/#{vector_store_id}") 17 | end 18 | 19 | def new(args = [_ | _]) do 20 | args |> Enum.into(%{}) |> new() 21 | end 22 | 23 | def new(args = %{}) do 24 | args |> Map.take(@api_fields) 25 | end 26 | 27 | def list!(openai = %OpenaiEx{}) do 28 | openai |> list() |> Http.bang_it!() 29 | end 30 | 31 | def list(openai = %OpenaiEx{}) do 32 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url()) 33 | end 34 | 35 | def create!(openai = %OpenaiEx{}, params \\ %{}) do 36 | openai |> create(params) |> Http.bang_it!() 37 | end 38 | 39 | def create(openai = %OpenaiEx{}, params \\ %{}) do 40 | json = params |> Map.take(@api_fields) 41 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(), json: json) 42 | end 43 | 44 | def retrieve!(openai = %OpenaiEx{}, vector_store_id) do 45 | openai |> retrieve(vector_store_id) |> Http.bang_it!() 46 | end 47 | 48 | def retrieve(openai = %OpenaiEx{}, vector_store_id) do 49 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(vector_store_id)) 50 | end 51 | 52 | def update!(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 53 | openai |> update(vector_store_id, params) |> Http.bang_it!() 54 | end 55 | 56 | def update(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 57 | json = params |> Map.take([:name, :expires_after, :metadata]) 58 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(vector_store_id), json: json) 59 | end 60 | 61 | def delete!(openai = %OpenaiEx{}, vector_store_id) do 62 | openai |> delete(vector_store_id) |> Http.bang_it!() 63 | end 64 | 65 | def delete(openai = %OpenaiEx{}, vector_store_id) do 66 | openai |> OpenaiEx.with_assistants_beta() |> Http.delete(ep_url(vector_store_id)) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /.llm-context/rules/sty-elixir.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: sty-elixir 3 | description: Provides Elixir-specific style guidelines emphasizing functional programming patterns, OTP principles, pattern matching, and "The Elixir Way". Use for Elixir projects to ensure idiomatic, maintainable, and concurrent code. 4 | --- 5 | 6 | ## Elixir-Specific Guidelines 7 | 8 | ### Functional Programming Patterns 9 | 10 | - Prefer pattern matching over conditional logic (`case`, `with`, function heads) 11 | - Use pipe operators (`|>`) for clear data transformations and function composition 12 | - Leverage recursion and `Enum` functions instead of imperative loops 13 | - Keep functions pure and composable - avoid side effects in business logic 14 | - Use guards to express function preconditions clearly 15 | 16 | ### Data Flow and Transformation 17 | 18 | - Design data pipelines with the pipe operator for readability 19 | - Use `Enum.map/2`, `Enum.filter/2`, `Enum.reduce/3` over manual recursion when appropriate 20 | - Prefer list comprehensions for complex transformations: `for x <- list, condition, do: transform(x)` 21 | - Use `Stream` for lazy evaluation of large or infinite data sets 22 | 23 | ### Error Handling 24 | 25 | - Use `with/else` for clean error handling in complex operations 26 | - Prefer `{:ok, result}` / `{:error, reason}` tuples for explicit error returns 27 | - Let processes crash and restart - embrace "let it crash" philosophy 28 | - Use `try/rescue` sparingly, only for external library integration 29 | 30 | ### OTP and Concurrency 31 | 32 | - Structure stateful operations around OTP principles (GenServer, Supervisor, etc.) 33 | - Design with immutability and message-passing concurrency in mind 34 | - Use `Task` for fire-and-forget operations and `Task.async/await` for coordinated concurrency 35 | - Prefer lightweight processes over threads - spawn liberally 36 | 37 | ### Code Organization 38 | 39 | - Use protocols for polymorphic behavior instead of inheritance 40 | - Organize modules by domain/context, not by technical layer 41 | - Keep modules focused - prefer many small modules over large ones 42 | - Use `use` and `import` judiciously - prefer explicit module calls for clarity 43 | 44 | ### Naming and Style 45 | 46 | - Use snake_case for functions, variables, and atoms 47 | - Use PascalCase for modules and protocols 48 | - Use descriptive names that express intent: `calculate_tax/1` not `calc/1` 49 | - Prefer explicit return values over implicit ones 50 | -------------------------------------------------------------------------------- /lib/openai_ex/batches.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Batches do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI Batch API. The API reference can be found at https://platform.openai.com/docs/api-reference/batch. 4 | """ 5 | 6 | alias OpenaiEx.Http 7 | 8 | @api_fields [ 9 | :input_file_id, 10 | :completion_window, 11 | :endpoint, 12 | :metadata, 13 | :after, 14 | :limit 15 | ] 16 | 17 | defp ep_url(batch_id \\ nil, action \\ nil) do 18 | "/batches" <> 19 | if(is_nil(batch_id), do: "", else: "/#{batch_id}") <> 20 | if(is_nil(action), do: "", else: "/#{action}") 21 | end 22 | 23 | @doc """ 24 | Creates a new batch request 25 | """ 26 | def new(args = [_ | _]) do 27 | args |> Enum.into(%{}) |> new() 28 | end 29 | 30 | def new(args = %{}) do 31 | args |> Map.take(@api_fields) 32 | end 33 | 34 | @doc """ 35 | Creates and executes a batch from an uploaded file of requests. 36 | 37 | https://platform.openai.com/docs/api-reference/batch/create 38 | """ 39 | def create!(openai = %OpenaiEx{}, batch = %{}) do 40 | openai |> create(batch) |> Http.bang_it!() 41 | end 42 | 43 | def create(openai = %OpenaiEx{}, batch = %{}) do 44 | openai |> Http.post(ep_url(), json: batch) 45 | end 46 | 47 | @doc """ 48 | Retrieves a batch. 49 | 50 | https://platform.openai.com/docs/api-reference/batch/retrieve 51 | """ 52 | def retrieve!(openai = %OpenaiEx{}, batch_id: batch_id) do 53 | openai |> retrieve(batch_id: batch_id) |> Http.bang_it!() 54 | end 55 | 56 | def retrieve(openai = %OpenaiEx{}, batch_id: batch_id) do 57 | openai |> Http.get(ep_url(batch_id)) 58 | end 59 | 60 | @doc """ 61 | Cancels an in-progress batch. 62 | 63 | https://platform.openai.com/docs/api-reference/batch/cancel 64 | """ 65 | def cancel!(openai = %OpenaiEx{}, batch_id: batch_id) do 66 | openai |> cancel(batch_id: batch_id) |> Http.bang_it!() 67 | end 68 | 69 | def cancel(openai = %OpenaiEx{}, batch_id: batch_id) do 70 | openai |> Http.post(ep_url(batch_id, "cancel")) 71 | end 72 | 73 | @doc """ 74 | Lists your organization's batches. 75 | 76 | https://platform.openai.com/docs/api-reference/batch/list 77 | """ 78 | def list!(openai = %OpenaiEx{}, opts \\ []) do 79 | openai |> list(opts) |> Http.bang_it!() 80 | end 81 | 82 | def list(openai = %OpenaiEx{}, opts \\ []) do 83 | params = opts |> Enum.into(%{}) |> Map.take([:after, :limit]) 84 | openai |> Http.get(ep_url(), params) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/openai_ex/vector_stores.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.VectorStores do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI vector_stores API. The API reference can be found at https://platform.openai.com/docs/api-reference/vector-stores. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :file_ids, 9 | :name, 10 | :expires_after, 11 | :metadata 12 | ] 13 | 14 | defp ep_url(vector_store_id \\ nil) do 15 | "/vector_stores" <> 16 | if(is_nil(vector_store_id), do: "", else: "/#{vector_store_id}") 17 | end 18 | 19 | @doc """ 20 | Creates a new vector store request 21 | 22 | Example usage: 23 | 24 | iex> _request = OpenaiEx.VectorStores.new(name: "My Vector Store", file_ids: ["file-abc123"]) 25 | %{name: "My Vector Store", file_ids: ["file-abc123"]} 26 | 27 | iex> _request = OpenaiEx.VectorStores.new(%{name: "My Vector Store", metadata: %{"version" => "1.0"}}) 28 | %{name: "My Vector Store", metadata: %{"version" => "1.0"}} 29 | """ 30 | def new(args = [_ | _]) do 31 | args |> Enum.into(%{}) |> new() 32 | end 33 | 34 | def new(args = %{}) do 35 | args |> Map.take(@api_fields) 36 | end 37 | 38 | def list!(openai = %OpenaiEx{}) do 39 | openai |> list() |> Http.bang_it!() 40 | end 41 | 42 | def list(openai = %OpenaiEx{}) do 43 | openai |> Http.get(ep_url()) 44 | end 45 | 46 | def create!(openai = %OpenaiEx{}, params \\ %{}) do 47 | openai |> create(params) |> Http.bang_it!() 48 | end 49 | 50 | def create(openai = %OpenaiEx{}, params \\ %{}) do 51 | json = params |> Map.take(@api_fields) 52 | openai |> Http.post(ep_url(), json: json) 53 | end 54 | 55 | def retrieve!(openai = %OpenaiEx{}, vector_store_id) do 56 | openai |> retrieve(vector_store_id) |> Http.bang_it!() 57 | end 58 | 59 | def retrieve(openai = %OpenaiEx{}, vector_store_id) do 60 | openai |> Http.get(ep_url(vector_store_id)) 61 | end 62 | 63 | def update!(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 64 | openai |> update(vector_store_id, params) |> Http.bang_it!() 65 | end 66 | 67 | def update(openai = %OpenaiEx{}, vector_store_id, params \\ %{}) do 68 | json = params |> Map.take([:name, :expires_after, :metadata]) 69 | openai |> Http.post(ep_url(vector_store_id), json: json) 70 | end 71 | 72 | def delete!(openai = %OpenaiEx{}, vector_store_id) do 73 | openai |> delete(vector_store_id) |> Http.bang_it!() 74 | end 75 | 76 | def delete(openai = %OpenaiEx{}, vector_store_id) do 77 | openai |> Http.delete(ep_url(vector_store_id)) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/openai_ex/conversations.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Conversations do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI Conversations API. The API reference can be found at https://platform.openai.com/docs/api-reference/conversations. 4 | """ 5 | 6 | alias OpenaiEx.Http 7 | 8 | @api_fields [ 9 | :metadata, 10 | :items 11 | ] 12 | 13 | defp ep_url(conversation_id \\ nil) do 14 | "/conversations" <> 15 | if(is_nil(conversation_id), do: "", else: "/#{conversation_id}") 16 | end 17 | 18 | @doc """ 19 | Creates a new conversation request with the given arguments. 20 | 21 | ## Examples 22 | 23 | iex> OpenaiEx.Conversations.new(metadata: %{"user_id" => "123"}) 24 | %{metadata: %{"user_id" => "123"}} 25 | 26 | iex> OpenaiEx.Conversations.new(items: [%{type: "message", role: "user", content: [%{type: "input_text", text: "Hello"}]}]) 27 | %{items: [%{type: "message", role: "user", content: [%{type: "input_text", text: "Hello"}]}]} 28 | 29 | iex> OpenaiEx.Conversations.new([]) 30 | %{} 31 | """ 32 | def new(args) when is_list(args) do 33 | args |> Enum.into(%{}) |> new() 34 | end 35 | 36 | def new(args) when is_map(args) do 37 | args |> Map.take(@api_fields) 38 | end 39 | 40 | @doc """ 41 | Creates a new conversation. 42 | 43 | See https://platform.openai.com/docs/api-reference/conversations/create 44 | """ 45 | def create!(openai = %OpenaiEx{}, request \\ %{}) do 46 | openai |> create(request) |> Http.bang_it!() 47 | end 48 | 49 | def create(openai = %OpenaiEx{}, request \\ %{}) do 50 | openai |> Http.post(ep_url(), json: request) 51 | end 52 | 53 | @doc """ 54 | Retrieves a specific conversation by ID. 55 | 56 | See https://platform.openai.com/docs/api-reference/conversations/retrieve 57 | """ 58 | def retrieve!(openai = %OpenaiEx{}, conversation_id) do 59 | openai |> retrieve(conversation_id) |> Http.bang_it!() 60 | end 61 | 62 | def retrieve(openai = %OpenaiEx{}, conversation_id) do 63 | openai |> Http.get(ep_url(conversation_id)) 64 | end 65 | 66 | @doc """ 67 | Updates a conversation's metadata. 68 | 69 | See https://platform.openai.com/docs/api-reference/conversations/update 70 | """ 71 | def update!(openai = %OpenaiEx{}, conversation_id, request) do 72 | openai |> update(conversation_id, request) |> Http.bang_it!() 73 | end 74 | 75 | def update(openai = %OpenaiEx{}, conversation_id, request) do 76 | openai |> Http.post(ep_url(conversation_id), json: request) 77 | end 78 | 79 | @doc """ 80 | Deletes a conversation. 81 | 82 | See https://platform.openai.com/docs/api-reference/conversations/delete 83 | """ 84 | def delete!(openai = %OpenaiEx{}, conversation_id) do 85 | openai |> delete(conversation_id) |> Http.bang_it!() 86 | end 87 | 88 | def delete(openai = %OpenaiEx{}, conversation_id) do 89 | openai |> Http.delete(ep_url(conversation_id)) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/openai_ex/containers.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Containers do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI Containers API. The API reference can be found at https://platform.openai.com/docs/api-reference/containers. 4 | """ 5 | 6 | alias OpenaiEx.Http 7 | 8 | @api_fields [ 9 | :name, 10 | :expires_after, 11 | :file_ids, 12 | :container_id 13 | ] 14 | 15 | defp ep_url(container_id \\ nil) do 16 | "/containers" <> 17 | if(is_nil(container_id), do: "", else: "/#{container_id}") 18 | end 19 | 20 | @doc """ 21 | Creates a new container request with the given arguments. 22 | 23 | ## Examples 24 | 25 | iex> OpenaiEx.Containers.new(name: "My Container") 26 | %{name: "My Container"} 27 | 28 | iex> OpenaiEx.Containers.new(name: "Test Container", expires_after: %{anchor: "last_active_at", minutes: 30}) 29 | %{name: "Test Container", expires_after: %{anchor: "last_active_at", minutes: 30}} 30 | 31 | iex> OpenaiEx.Containers.new(name: "File Container", file_ids: ["file-123", "file-456"]) 32 | %{name: "File Container", file_ids: ["file-123", "file-456"]} 33 | """ 34 | def new(args) when is_list(args) do 35 | args |> Enum.into(%{}) |> new() 36 | end 37 | 38 | def new(args) when is_map(args) do 39 | args |> Map.take(@api_fields) 40 | end 41 | 42 | @doc """ 43 | Lists all containers that belong to the user's organization. 44 | 45 | See https://platform.openai.com/docs/api-reference/containers/listContainers 46 | """ 47 | def list!(openai = %OpenaiEx{}, params \\ %{}) do 48 | openai |> list(params) |> Http.bang_it!() 49 | end 50 | 51 | def list(openai = %OpenaiEx{}, params \\ %{}) do 52 | query_params = params |> Map.take(OpenaiEx.list_query_fields()) 53 | openai |> Http.get(ep_url(), query_params) 54 | end 55 | 56 | @doc """ 57 | Creates a new container. 58 | 59 | See https://platform.openai.com/docs/api-reference/containers/createContainers 60 | """ 61 | def create!(openai = %OpenaiEx{}, request) do 62 | openai |> create(request) |> Http.bang_it!() 63 | end 64 | 65 | def create(openai = %OpenaiEx{}, request) do 66 | openai |> Http.post(ep_url(), json: request) 67 | end 68 | 69 | @doc """ 70 | Retrieves a specific container by ID. 71 | 72 | See https://platform.openai.com/docs/api-reference/containers/retrieveContainer 73 | """ 74 | def retrieve!(openai = %OpenaiEx{}, container_id) do 75 | openai |> retrieve(container_id) |> Http.bang_it!() 76 | end 77 | 78 | def retrieve(openai = %OpenaiEx{}, container_id) do 79 | openai |> Http.get(ep_url(container_id)) 80 | end 81 | 82 | @doc """ 83 | Deletes a container. 84 | 85 | See https://platform.openai.com/docs/api-reference/containers/deleteContainer 86 | """ 87 | def delete!(openai = %OpenaiEx{}, container_id) do 88 | openai |> delete(container_id) |> Http.bang_it!() 89 | end 90 | 91 | def delete(openai = %OpenaiEx{}, container_id) do 92 | openai |> Http.delete(ep_url(container_id)) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/openai_ex/conversation_items.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.ConversationItems do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI Conversation Items API. The API reference can be found at https://platform.openai.com/docs/api-reference/conversations/items. 4 | """ 5 | 6 | alias OpenaiEx.Http 7 | 8 | @api_fields [ 9 | :items 10 | ] 11 | 12 | defp ep_url(conversation_id, item_id \\ nil) do 13 | base = "/conversations/#{conversation_id}/items" 14 | 15 | if is_nil(item_id), do: base, else: "#{base}/#{item_id}" 16 | end 17 | 18 | @doc """ 19 | Creates a new conversation items request with the given arguments. 20 | 21 | ## Examples 22 | 23 | iex> OpenaiEx.ConversationItems.new(items: [%{type: "message", role: "user", content: [%{type: "input_text", text: "Hello"}]}]) 24 | %{items: [%{type: "message", role: "user", content: [%{type: "input_text", text: "Hello"}]}]} 25 | """ 26 | def new(args) when is_list(args) do 27 | args |> Enum.into(%{}) |> new() 28 | end 29 | 30 | def new(args) when is_map(args) do 31 | args |> Map.take(@api_fields) 32 | end 33 | 34 | @doc """ 35 | Creates conversation items (one or more messages). 36 | 37 | See https://platform.openai.com/docs/api-reference/conversations/createItem 38 | """ 39 | def create!(openai = %OpenaiEx{}, conversation_id, request) do 40 | openai |> create(conversation_id, request) |> Http.bang_it!() 41 | end 42 | 43 | def create(openai = %OpenaiEx{}, conversation_id, request) do 44 | openai |> Http.post(ep_url(conversation_id), json: request) 45 | end 46 | 47 | @doc """ 48 | Retrieves a specific conversation item by ID. 49 | 50 | See https://platform.openai.com/docs/api-reference/conversations/retrieveItem 51 | """ 52 | def retrieve!(openai = %OpenaiEx{}, conversation_id, item_id) do 53 | openai |> retrieve(conversation_id, item_id) |> Http.bang_it!() 54 | end 55 | 56 | def retrieve(openai = %OpenaiEx{}, conversation_id, item_id) do 57 | openai |> Http.get(ep_url(conversation_id, item_id)) 58 | end 59 | 60 | @doc """ 61 | Lists all items in a conversation. 62 | 63 | See https://platform.openai.com/docs/api-reference/conversations/listItems 64 | """ 65 | def list!(openai = %OpenaiEx{}, conversation_id, params \\ %{}) do 66 | openai |> list(conversation_id, params) |> Http.bang_it!() 67 | end 68 | 69 | def list(openai = %OpenaiEx{}, conversation_id, params \\ %{}) do 70 | query_params = params |> Map.take(OpenaiEx.list_query_fields()) 71 | openai |> Http.get(ep_url(conversation_id), query_params) 72 | end 73 | 74 | @doc """ 75 | Deletes a conversation item. 76 | 77 | See https://platform.openai.com/docs/api-reference/conversations/deleteItem 78 | """ 79 | def delete!(openai = %OpenaiEx{}, conversation_id, item_id) do 80 | openai |> delete(conversation_id, item_id) |> Http.bang_it!() 81 | end 82 | 83 | def delete(openai = %OpenaiEx{}, conversation_id, item_id) do 84 | openai |> Http.delete(ep_url(conversation_id, item_id)) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/threads.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.Threads do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI threads API. The API reference can be found at https://platform.openai.com/docs/api-reference/threads. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :messages, 9 | :tool_resources, 10 | :metadata 11 | ] 12 | 13 | defp ep_url(thread_id \\ nil) do 14 | "/threads" <> if is_nil(thread_id), do: "", else: "/#{thread_id}" 15 | end 16 | 17 | @doc """ 18 | Creates a new threads request 19 | """ 20 | 21 | def new(args = [_ | _]) do 22 | args |> Enum.into(%{}) |> new() 23 | end 24 | 25 | def new(args = %{}) do 26 | args |> Map.take(@api_fields) 27 | end 28 | 29 | @doc """ 30 | Calls the thread create endpoint. 31 | 32 | https://platform.openai.com/docs/api-reference/threads/createThread 33 | """ 34 | def create!(openai = %OpenaiEx{}, params = %{} \\ %{}) do 35 | openai |> create(params) |> Http.bang_it!() 36 | end 37 | 38 | def create(openai = %OpenaiEx{}, params = %{} \\ %{}) do 39 | json = params |> Map.take(@api_fields) 40 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(), json: json) 41 | end 42 | 43 | @doc """ 44 | Calls the thread retrieve endpoint. 45 | 46 | https://platform.openai.com/docs/api-reference/threads/getThread 47 | """ 48 | def retrieve!(openai = %OpenaiEx{}, thread_id) do 49 | openai |> retrieve(thread_id) |> Http.bang_it!() 50 | end 51 | 52 | def retrieve(openai = %OpenaiEx{}, thread_id) do 53 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(thread_id)) 54 | end 55 | 56 | @doc """ 57 | Calls the thread update endpoint. 58 | 59 | https://platform.openai.com/docs/api-reference/threads/modifyThread 60 | """ 61 | def update!(openai = %OpenaiEx{}, thread_id, params = %{metadata: _metadata}) do 62 | openai |> update(thread_id, params) |> Http.bang_it!() 63 | end 64 | 65 | def update(openai = %OpenaiEx{}, thread_id, params = %{metadata: _metadata}) do 66 | json = params |> Map.take([:thread_id, :metadata]) 67 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(thread_id), json: json) 68 | end 69 | 70 | @doc """ 71 | Calls the thread delete endpoint. 72 | 73 | https://platform.openai.com/docs/api-reference/threads/deleteThread 74 | """ 75 | def delete!(openai = %OpenaiEx{}, thread_id) do 76 | openai |> delete(thread_id) |> Http.bang_it!() 77 | end 78 | 79 | def delete(openai = %OpenaiEx{}, thread_id) do 80 | openai |> OpenaiEx.with_assistants_beta() |> Http.delete(ep_url(thread_id)) 81 | end 82 | 83 | # Not (yet) part of the documented API, but the endpoint exists. 84 | @doc false 85 | def list!(openai = %OpenaiEx{}, params = %{} \\ %{}) do 86 | openai |> list(params) |> Http.bang_it!() 87 | end 88 | 89 | def list(openai = %OpenaiEx{}, params = %{} \\ %{}) do 90 | qry_params = params |> Map.take(OpenaiEx.list_query_fields()) 91 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(), qry_params) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/sty-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Outlines universal code style principles for modern programming languages, emphasizing functional patterns, clarity, immutability, and robust architecture. Use as a foundation for language-agnostic coding. 3 | --- 4 | 5 | ## Universal Code Style Principles 6 | 7 | ### Functional Programming Approach 8 | 9 | - Prefer functional over imperative patterns 10 | - Favor pure functions and immutable data structures 11 | - Design for method chaining through immutable transformations 12 | - Prefer conditional expressions over conditional statements when possible 13 | 14 | ### Code Clarity 15 | 16 | - Write self-documenting code through expressive naming 17 | - Good names should make comments superfluous 18 | - Compose complex operations through small, focused functions 19 | 20 | ### Object Design 21 | 22 | - Keep constructors/initializers simple with minimal logic 23 | - Use static factory methods (typically `create()`) for complex object construction 24 | - Design methods to return new instances rather than mutating state 25 | - Prefer immutable data structures and frozen/sealed objects 26 | 27 | ### Error Handling 28 | 29 | - Validate inputs at application boundaries, not within internal functions 30 | - **Natural Failure Over Validation**: Don't add explicit checks for conditions that will naturally cause failures 31 | - Let language built-in error mechanisms work (TypeError, ReferenceError, etc.) 32 | - Only validate at true application boundaries (user input, external APIs) 33 | - Internal functions should assume valid inputs and fail fast 34 | - Trust that calling code has met preconditions - fail fast if not 35 | - Avoid defensive programming within core logic 36 | - Create clear contracts between functions 37 | 38 | ### Architecture 39 | 40 | - Favor composition over inheritance 41 | - Avoid static dependencies - use dependency injection for testability 42 | - Maintain clear separation between pure logic and side effects 43 | 44 | **Goal: Write beautiful code that is readable, maintainable, and robust.** 45 | 46 | ## Code Quality Enforcement 47 | 48 | **CRITICAL: Follow all style guidelines rigorously in every code response.** 49 | 50 | Before writing any code: 51 | 1. **Check functional patterns** - Are functions pure? Do they return new data instead of mutating? 52 | 2. **Review naming** - Are names concise but expressive? Avoid verbose parameter names. 53 | 3. **Verify immutability** - Are data structures immutable? Can operations be chained? 54 | 4. **Simplify logic** - Can this be written more elegantly with comprehensions, functional patterns? 55 | 5. **Type hints** - Are all parameters and returns properly typed? 56 | 57 | **Red flags that indicate style violations:** 58 | - Functions that mutate input parameters 59 | - Verbose parameter names like `coverage_threshold` vs `threshold` 60 | - Imperative loops instead of functional patterns 61 | - Missing type hints or vague types like `Any` 62 | - Complex nested conditionals instead of guard clauses 63 | 64 | **When in doubt, prioritize elegance and functional patterns over apparent convenience.** 65 | -------------------------------------------------------------------------------- /lib/openai_ex/files.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Files do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI files API. The API reference can be found at https://platform.openai.com/docs/api-reference/files. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :file, 9 | :purpose, 10 | :file_id 11 | ] 12 | 13 | defp ep_url(file_id \\ nil, action \\ nil) do 14 | "/files" <> 15 | if(is_nil(file_id), do: "", else: "/#{file_id}") <> 16 | if(is_nil(action), do: "", else: "/#{action}") 17 | end 18 | 19 | @doc """ 20 | Creates a new file upload request with the given arguments. 21 | """ 22 | def new_upload(args = [_ | _]) do 23 | args |> Enum.into(%{}) |> new_upload() 24 | end 25 | 26 | def new_upload(args = %{file: _, purpose: _}) do 27 | args |> Map.take(@api_fields) 28 | end 29 | 30 | @doc """ 31 | Creates a new file retrieve / deletion / retrieve_content request 32 | """ 33 | def new(args = [_ | _]) do 34 | args |> Enum.into(%{}) |> new() 35 | end 36 | 37 | def new(args = %{file_id: _}) do 38 | args |> Map.take(@api_fields) 39 | end 40 | 41 | @doc """ 42 | Lists the files that belong to the user's organization. 43 | 44 | https://platform.openai.com/docs/api-reference/files/list 45 | """ 46 | def list!(openai = %OpenaiEx{}) do 47 | openai |> list() |> Http.bang_it!() 48 | end 49 | 50 | def list(openai = %OpenaiEx{}) do 51 | openai |> Http.get(ep_url()) 52 | end 53 | 54 | @doc """ 55 | Calls the file upload endpoint. 56 | 57 | https://platform.openai.com/docs/api-reference/files/upload 58 | """ 59 | def create!(openai = %OpenaiEx{}, upload) do 60 | openai |> create(upload) |> Http.bang_it!() 61 | end 62 | 63 | def create(openai = %OpenaiEx{}, upload) do 64 | multipart = upload |> Http.to_multi_part_form_data(file_fields()) 65 | openai |> Http.post(ep_url(), multipart: multipart) 66 | end 67 | 68 | @doc """ 69 | Calls the file delete endpoint. 70 | 71 | https://platform.openai.com/docs/api-reference/files/delete 72 | """ 73 | def delete!(openai = %OpenaiEx{}, file_id) do 74 | openai |> delete(file_id) |> Http.bang_it!() 75 | end 76 | 77 | def delete(openai = %OpenaiEx{}, file_id) do 78 | openai |> Http.delete(ep_url(file_id)) 79 | end 80 | 81 | @doc """ 82 | Calls the file retrieve endpoint. 83 | 84 | https://platform.openai.com/docs/api-reference/files/retrieve 85 | """ 86 | def retrieve!(openai = %OpenaiEx{}, file_id) do 87 | openai |> retrieve(file_id) |> Http.bang_it!() 88 | end 89 | 90 | def retrieve(openai = %OpenaiEx{}, file_id) do 91 | openai |> Http.get(ep_url(file_id)) 92 | end 93 | 94 | @doc """ 95 | Calls the file retrieve_content endpoint. 96 | 97 | https://platform.openai.com/docs/api-reference/files/retrieve-content 98 | """ 99 | def content!(openai = %OpenaiEx{}, file_id) do 100 | openai |> content(file_id) |> Http.bang_it!() 101 | end 102 | 103 | def content(openai = %OpenaiEx{}, file_id) do 104 | openai |> Http.get_no_decode(ep_url(file_id, "content")) 105 | end 106 | 107 | @doc false 108 | def file_fields() do 109 | [:file] 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/openai_ex/fine_tuning_jobs.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.FineTuning.Jobs do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI fine-tuning job API. The API reference can be found at https://platform.openai.com/docs/api-reference/fine-tuning. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :model, 9 | :hyperparameters, 10 | :integrations, 11 | :suffix, 12 | :seed, 13 | :training_file, 14 | :validation_file, 15 | :after, 16 | :limit 17 | ] 18 | 19 | defp ep_url(fine_tuning_job_id \\ nil, action \\ nil) do 20 | "/fine_tuning/jobs" <> 21 | if(is_nil(fine_tuning_job_id), do: "", else: "/#{fine_tuning_job_id}") <> 22 | if(is_nil(action), do: "", else: "/#{action}") 23 | end 24 | 25 | @doc """ 26 | Creates a new fine-tuning job request 27 | """ 28 | def new(args = [_ | _]) do 29 | args |> Enum.into(%{}) |> new() 30 | end 31 | 32 | def new(args = %{}) do 33 | args |> Map.take(@api_fields) 34 | end 35 | 36 | @doc """ 37 | Calls the fine-tuning job list endpoint. 38 | 39 | https://platform.openai.com/docs/api-reference/fine-tuning/list 40 | """ 41 | def list!(openai = %OpenaiEx{}, params = %{} \\ %{}) do 42 | openai |> list(params) |> Http.bang_it!() 43 | end 44 | 45 | def list(openai = %OpenaiEx{}, params = %{} \\ %{}) do 46 | openai |> Http.get(ep_url(), params |> Map.take(@api_fields)) 47 | end 48 | 49 | @doc """ 50 | Calls the fine-tuning job creation endpoint. 51 | 52 | https://platform.openai.com/docs/api-reference/fine-tuning/create 53 | """ 54 | def create!(openai = %OpenaiEx{}, finetuning = %{}) do 55 | openai |> create(finetuning) |> Http.bang_it!() 56 | end 57 | 58 | def create(openai = %OpenaiEx{}, finetuning = %{}) do 59 | openai |> Http.post(ep_url(), json: finetuning) 60 | end 61 | 62 | @doc """ 63 | Calls the fine-tuning job retrieve endpoint. 64 | 65 | https://platform.openai.com/docs/api-reference/fine-tuning/retrieve 66 | """ 67 | def retrieve!(openai = %OpenaiEx{}, fine_tuning_job_id: fine_tuning_job_id) do 68 | openai |> retrieve(fine_tuning_job_id: fine_tuning_job_id) |> Http.bang_it!() 69 | end 70 | 71 | def retrieve(openai = %OpenaiEx{}, fine_tuning_job_id: fine_tuning_job_id) do 72 | openai |> Http.get(ep_url(fine_tuning_job_id)) 73 | end 74 | 75 | @doc """ 76 | Calls the fine-tuning job cancel endpoint. 77 | 78 | https://platform.openai.com/docs/api-reference/fine-tuning/cancel 79 | """ 80 | def cancel!(openai = %OpenaiEx{}, fine_tuning_job_id: fine_tuning_job_id) do 81 | openai |> cancel(fine_tuning_job_id: fine_tuning_job_id) |> Http.bang_it!() 82 | end 83 | 84 | def cancel(openai = %OpenaiEx{}, fine_tuning_job_id: fine_tuning_job_id) do 85 | openai |> Http.post(ep_url(fine_tuning_job_id, "cancel")) 86 | end 87 | 88 | @doc """ 89 | Calls the fine-tuning job list events endpoint. 90 | 91 | https://platform.openai.com/docs/api-reference/fine-tuning/events 92 | """ 93 | def list_events!(openai = %OpenaiEx{}, opts) do 94 | openai |> list_events(opts) |> Http.bang_it!() 95 | end 96 | 97 | def list_events(openai = %OpenaiEx{}, opts = [fine_tuning_job_id: fine_tuning_job_id]) do 98 | params = opts |> Enum.into(%{}) |> Map.take([:after, :limit]) 99 | openai |> Http.get(ep_url(fine_tuning_job_id, "events"), params) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/openai_ex/responses.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Responses do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI Responses API. 4 | The API reference can be found at https://platform.openai.com/docs/api-reference/responses. 5 | """ 6 | alias OpenaiEx.{Http, HttpSse} 7 | 8 | @api_fields [ 9 | :model, 10 | :background, 11 | :conversation, 12 | :include, 13 | :input, 14 | :instructions, 15 | :max_output_tokens, 16 | :max_tool_calls, 17 | :metadata, 18 | :parallel_tool_calls, 19 | :previous_response_id, 20 | :prompt, 21 | :prompt_cache_key, 22 | :reasoning, 23 | :safety_identifier, 24 | :service_tier, 25 | :store, 26 | :stream_options, 27 | :temperature, 28 | :text, 29 | :tool_choice, 30 | :tools, 31 | :top_logprobs, 32 | :top_p, 33 | :truncation, 34 | :user 35 | ] 36 | 37 | @query_params [ 38 | :include 39 | ] 40 | 41 | defp ep_url(response_id \\ nil) do 42 | "/responses" <> if(is_nil(response_id), do: "", else: "/#{response_id}") 43 | end 44 | 45 | @doc """ 46 | Creates a new response. 47 | 48 | https://platform.openai.com/docs/api-reference/responses/create 49 | """ 50 | def create!(openai = %OpenaiEx{}, params, stream: true) do 51 | openai |> create(params, stream: true) |> Http.bang_it!() 52 | end 53 | 54 | def create(openai = %OpenaiEx{}, params, stream: true) do 55 | request_body = params |> Map.take(@api_fields) |> Map.put(:stream, true) 56 | openai |> HttpSse.post(ep_url(), json: request_body) 57 | end 58 | 59 | def create!(openai = %OpenaiEx{}, params) do 60 | openai |> create(params) |> Http.bang_it!() 61 | end 62 | 63 | def create(openai = %OpenaiEx{}, params) do 64 | request_body = params |> Map.take(@api_fields) 65 | openai |> Http.post(ep_url(), json: request_body) 66 | end 67 | 68 | @doc """ 69 | Retrieves a response. 70 | 71 | https://platform.openai.com/docs/api-reference/responses/retrieve 72 | """ 73 | def retrieve!(openai = %OpenaiEx{}, opts) when is_list(opts) do 74 | openai |> retrieve(opts) |> Http.bang_it!() 75 | end 76 | 77 | def retrieve(openai = %OpenaiEx{}, opts) when is_list(opts) do 78 | response_id = Keyword.fetch!(opts, :response_id) 79 | params = opts |> Keyword.drop([:response_id]) |> Enum.into(%{}) |> Map.take(@query_params) 80 | openai |> Http.get(ep_url(response_id), params) 81 | end 82 | 83 | @doc """ 84 | Deletes a response. 85 | 86 | See https://platform.openai.com/docs/api-reference/responses/delete 87 | """ 88 | def delete!(openai = %OpenaiEx{}, response_id: response_id) do 89 | openai |> delete(response_id: response_id) |> Http.bang_it!() 90 | end 91 | 92 | def delete(openai = %OpenaiEx{}, response_id: response_id) do 93 | openai |> Http.delete(ep_url(response_id)) 94 | end 95 | 96 | @doc """ 97 | Lists input items from a response. See https://platform.openai.com/docs/api-reference/responses/input-items 98 | """ 99 | def input_items_list!(openai = %OpenaiEx{}, opts) when is_list(opts) do 100 | openai |> input_items_list(opts) |> Http.bang_it!() 101 | end 102 | 103 | def input_items_list(openai = %OpenaiEx{}, opts) when is_list(opts) do 104 | response_id = Keyword.fetch!(opts, :response_id) 105 | 106 | p = 107 | opts 108 | |> Keyword.drop([:response_id]) 109 | |> Enum.into(%{}) 110 | |> Map.take(OpenaiEx.list_query_fields()) 111 | 112 | openai |> Http.get(ep_url(response_id) <> "/input_items", p) 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/threads_messages.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.Threads.Messages do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI messages API. The API 4 | reference can be found at https://platform.openai.com/docs/api-reference/messages. 5 | """ 6 | alias OpenaiEx.Http 7 | 8 | @api_fields [ 9 | :role, 10 | :content, 11 | :attachments, 12 | :metadata 13 | ] 14 | 15 | defp ep_url(thread_id, message_id \\ nil) do 16 | "/threads/#{thread_id}/messages" <> if is_nil(message_id), do: "", else: "/#{message_id}" 17 | end 18 | 19 | @doc """ 20 | Creates a new message request 21 | """ 22 | 23 | def new(args = [_ | _]) do 24 | args |> Enum.into(%{}) |> new() 25 | end 26 | 27 | def new(args) do 28 | args |> Map.take([:thread_id | [:message_id | @api_fields]]) 29 | end 30 | 31 | @doc """ 32 | Calls the message create endpoint. 33 | 34 | https://platform.openai.com/docs/api-reference/messages/createMessage 35 | """ 36 | def create!(openai = %OpenaiEx{}, thread_id, params) do 37 | openai |> create(thread_id, params) |> Http.bang_it!() 38 | end 39 | 40 | def create(openai = %OpenaiEx{}, thread_id, params = %{role: _, content: _}) do 41 | json = params |> Map.take(@api_fields) 42 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(thread_id), json: json) 43 | end 44 | 45 | @doc """ 46 | Calls the message retrieve endpoint. 47 | 48 | https://platform.openai.com/docs/api-reference/messages/getMessage 49 | """ 50 | def retrieve!(openai = %OpenaiEx{}, params = %{thread_id: _, message_id: _}) do 51 | openai |> retrieve(params) |> Http.bang_it!() 52 | end 53 | 54 | def retrieve(openai = %OpenaiEx{}, %{thread_id: thread_id, message_id: message_id}) do 55 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(thread_id, message_id)) 56 | end 57 | 58 | @doc """ 59 | Calls the message update endpoint. 60 | 61 | https://platform.openai.com/docs/api-reference/messages/modifyMessage 62 | """ 63 | def update!(openai = %OpenaiEx{}, params = %{thread_id: _, message_id: _, metadata: _}) do 64 | openai |> update(params) |> Http.bang_it!() 65 | end 66 | 67 | def update(openai = %OpenaiEx{}, %{ 68 | thread_id: thread_id, 69 | message_id: message_id, 70 | metadata: metadata 71 | }) do 72 | openai 73 | |> OpenaiEx.with_assistants_beta() 74 | |> Http.post(ep_url(thread_id, message_id), json: %{metadata: metadata}) 75 | end 76 | 77 | def delete!(openai = %OpenaiEx{}, thread_id, message_id) do 78 | openai |> delete(thread_id, message_id) |> Http.bang_it!() 79 | end 80 | 81 | def delete(openai = %OpenaiEx{}, thread_id, message_id) do 82 | openai |> OpenaiEx.with_assistants_beta() |> Http.delete(ep_url(thread_id, message_id)) 83 | end 84 | 85 | @doc """ 86 | Lists the messages that belong to the specified thread. 87 | 88 | https://platform.openai.com/docs/api-reference/messages/listMessages 89 | """ 90 | 91 | def new_list(args = [_ | _]) do 92 | args |> Enum.into(%{}) |> new_list() 93 | end 94 | 95 | def new_list(args = %{thread_id: _thread_id}) do 96 | args |> Map.take([:thread_id | OpenaiEx.list_query_fields()]) 97 | end 98 | 99 | def list!(openai = %OpenaiEx{}, thread_id, params = %{} \\ %{}) do 100 | openai |> list(thread_id, params) |> Http.bang_it!() 101 | end 102 | 103 | def list(openai = %OpenaiEx{}, thread_id, params = %{} \\ %{}) do 104 | openai 105 | |> OpenaiEx.with_assistants_beta() 106 | |> Http.get(ep_url(thread_id), params |> Map.take([:run_id | OpenaiEx.list_query_fields()])) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/openai_ex/http.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Http do 2 | @moduledoc false 3 | alias OpenaiEx.HttpFinch 4 | 5 | def post(openai = %OpenaiEx{}, url) do 6 | post(openai, url, json: %{}) 7 | end 8 | 9 | def post(openai = %OpenaiEx{}, url, multipart: multipart) do 10 | HttpFinch.post(openai, url, multipart: multipart) |> handle_response() 11 | end 12 | 13 | def post(openai = %OpenaiEx{}, url, json: json) do 14 | HttpFinch.post(openai, url, json: json) |> handle_response() 15 | end 16 | 17 | def post_no_decode(openai = %OpenaiEx{}, url, json: json) do 18 | HttpFinch.post(openai, url, json: json) |> extract_body() 19 | end 20 | 21 | def get(openai = %OpenaiEx{}, base_url, params) do 22 | url = build_url(base_url, params) 23 | openai |> get(url) 24 | end 25 | 26 | def get(openai = %OpenaiEx{}, url) do 27 | HttpFinch.get(openai, url) |> handle_response() 28 | end 29 | 30 | def get_no_decode(openai = %OpenaiEx{}, url) do 31 | HttpFinch.get(openai, url) |> extract_body() 32 | end 33 | 34 | def delete(openai = %OpenaiEx{}, url) do 35 | HttpFinch.delete(openai, url) |> handle_response() 36 | end 37 | 38 | def to_multi_part_form_data(req, file_fields) do 39 | mp = 40 | req 41 | |> Map.drop(file_fields) 42 | |> Enum.reduce(Multipart.new(), fn {k, v}, acc -> 43 | acc |> Multipart.add_part(Multipart.Part.text_field(v, k)) 44 | end) 45 | 46 | req 47 | |> Map.take(file_fields) 48 | |> Enum.reduce(mp, fn {k, v}, acc -> 49 | case v do 50 | list when is_list(list) -> 51 | Enum.reduce(list, acc, fn item, inner_acc -> 52 | inner_acc |> Multipart.add_part(to_file_field_part("#{k}[]", item)) 53 | end) 54 | 55 | _ -> 56 | acc |> Multipart.add_part(to_file_field_part(k, v)) 57 | end 58 | end) 59 | end 60 | 61 | defp to_file_field_part(k, v) do 62 | case v do 63 | {path} -> 64 | Multipart.Part.file_field(path, k) 65 | 66 | {filename, content} -> 67 | Multipart.Part.file_content_field(filename, content, k, filename: filename) 68 | 69 | content -> 70 | Multipart.Part.file_content_field("", content, k, filename: "") 71 | end 72 | end 73 | 74 | def build_url(base_url, params \\ %{}) do 75 | prepared_params = prepare_query_params(params) 76 | 77 | if Enum.empty?(prepared_params) do 78 | base_url 79 | else 80 | base_url 81 | |> URI.new!() 82 | |> URI.append_query(prepared_params |> URI.encode_query()) 83 | |> URI.to_string() 84 | end 85 | end 86 | 87 | def prepare_query_params(params) when is_map(params) do 88 | Enum.flat_map(params, fn 89 | {key, values} when is_map(values) and key in [:metadata] -> 90 | Enum.map(values, fn {k, v} -> {"#{key}[#{k}]", v} end) 91 | 92 | {key, values} when is_list(values) and key in [:include] -> 93 | Enum.map(values, fn v -> {"#{key}[]", v} end) 94 | 95 | {key, value} -> 96 | [{key, value}] 97 | end) 98 | end 99 | 100 | defp handle_response(response) do 101 | response |> extract_body() |> jsonify() 102 | end 103 | 104 | defp extract_body(response) do 105 | case response do 106 | {:ok, response} -> {:ok, response.body} 107 | _ -> response 108 | end 109 | end 110 | 111 | defp jsonify(response) do 112 | case response do 113 | {:ok, response} -> {:ok, Jason.decode!(response)} 114 | _ -> response 115 | end 116 | end 117 | 118 | def bang_it!(response) do 119 | case response do 120 | {:ok, response} -> response 121 | {:error, error} -> raise error 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/assistants.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.Assistants do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI assistants API. The API reference can be found at https://platform.openai.com/docs/api-reference/assistants. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :model, 9 | :name, 10 | :description, 11 | :instructions, 12 | :tools, 13 | :tool_resources, 14 | :metadata, 15 | :temperature, 16 | :top_p, 17 | :response_format 18 | ] 19 | 20 | defp ep_url(assistant_id \\ nil) do 21 | "/assistants" <> if is_nil(assistant_id), do: "", else: "/#{assistant_id}" 22 | end 23 | 24 | @doc """ 25 | Creates a new assistants request 26 | 27 | Example usage: 28 | 29 | iex> _request = OpenaiEx.Beta.Assistants.new(model: "gpt-4-turbo") 30 | %{model: "gpt-4-turbo"} 31 | """ 32 | 33 | def new(args = [_ | _]) do 34 | args |> Enum.into(%{}) |> new() 35 | end 36 | 37 | def new(args = %{model: model}) do 38 | %{ 39 | model: model 40 | } 41 | |> Map.merge(args) 42 | |> Map.take(@api_fields) 43 | end 44 | 45 | @doc """ 46 | Calls the assistant 'create' endpoint. 47 | 48 | See https://platform.openai.com/docs/api-reference/assistants/createAssistant for more information. 49 | """ 50 | def create!(openai = %OpenaiEx{}, assistant = %{}) do 51 | openai |> create(assistant) |> Http.bang_it!() 52 | end 53 | 54 | def create(openai = %OpenaiEx{}, assistant = %{}) do 55 | json = assistant |> Map.take(@api_fields) 56 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(), json: json) 57 | end 58 | 59 | @doc """ 60 | Calls the assistant retrieve endpoint. 61 | 62 | https://platform.openai.com/docs/api-reference/assistants/getAssistant 63 | """ 64 | def retrieve!(openai = %OpenaiEx{}, assistant_id) do 65 | openai |> retrieve(assistant_id) |> Http.bang_it!() 66 | end 67 | 68 | def retrieve(openai = %OpenaiEx{}, assistant_id) do 69 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(assistant_id)) 70 | end 71 | 72 | @doc """ 73 | Calls the assistant update endpoint. 74 | 75 | See https://platform.openai.com/docs/api-reference/assistants/modifyAssistant for more information. 76 | """ 77 | def update!(openai = %OpenaiEx{}, assistant_id, assistant = %{}) do 78 | openai |> update(assistant_id, assistant) |> Http.bang_it!() 79 | end 80 | 81 | def update(openai = %OpenaiEx{}, assistant_id, assistant = %{}) do 82 | json = assistant |> Map.take(@api_fields) 83 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(assistant_id), json: json) 84 | end 85 | 86 | @doc """ 87 | Calls the assistant delete endpoint. 88 | 89 | https://platform.openai.com/docs/api-reference/assistants/deleteAssistant 90 | """ 91 | def delete!(openai = %OpenaiEx{}, assistant_id) do 92 | openai |> delete(assistant_id) |> Http.bang_it!() 93 | end 94 | 95 | def delete(openai = %OpenaiEx{}, assistant_id) do 96 | openai |> OpenaiEx.with_assistants_beta() |> Http.delete(ep_url(assistant_id)) 97 | end 98 | 99 | @doc """ 100 | Creates a new list assistants request 101 | """ 102 | 103 | def new_list(args = [_ | _]) do 104 | args |> Enum.into(%{}) |> new_list() 105 | end 106 | 107 | def new_list(args = %{}) do 108 | args |> Map.take(OpenaiEx.list_query_fields()) 109 | end 110 | 111 | @doc """ 112 | Returns a list of assistant objects. 113 | 114 | https://platform.openai.com/docs/api-reference/assistants/listAssistants 115 | """ 116 | def list!(openai = %OpenaiEx{}, params = %{} \\ %{}) do 117 | openai |> list(params) |> Http.bang_it!() 118 | end 119 | 120 | def list(openai = %OpenaiEx{}, params = %{} \\ %{}) do 121 | qry_params = params |> Map.take(OpenaiEx.list_query_fields()) 122 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(), qry_params) 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/flt-base.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Establishes base gitignore patterns to exclude non-code files (e.g., binaries, archives, logs) from overview, full, and outline selections. Use as a foundation for project-specific file filtering in context generation. 3 | gitignores: 4 | overview-files: 5 | - .git 6 | - "*.7z" 7 | - "*.app" 8 | - "*.avi" 9 | - "*.bmp" 10 | - "*.bz2" 11 | - "*.cab" 12 | - "*.deb" 13 | - "*.dll" 14 | - "*.dmg" 15 | - "*.dylib" 16 | - "*.ear" 17 | - "*.eot" 18 | - "*.exe" 19 | - "*.flac" 20 | - "*.gif" 21 | - "*.gz" 22 | - "*.icns" 23 | - "*.ico" 24 | - "*.iso" 25 | - "*.jar" 26 | - "*.jpeg" 27 | - "*.jpg" 28 | - "*.lz" 29 | - "*.lzma" 30 | - "*.map" 31 | - "*.mkv" 32 | - "*.mov" 33 | - "*.mp3" 34 | - "*.mp4" 35 | - "*.msi" 36 | - "*.otf" 37 | - "*.pdf" 38 | - "*.pkg" 39 | - "*.png" 40 | - "*.rar" 41 | - "*.rpm" 42 | - "*.so" 43 | - "*.svg" 44 | - "*.tar" 45 | - "*.tar.bz2" 46 | - "*.tar.gz" 47 | - "*.tar.xz" 48 | - "*.tbz2" 49 | - "*.tgz" 50 | - "*.tif" 51 | - "*.ttf" 52 | - "*.txz" 53 | - "*.war" 54 | - "*.wav" 55 | - "*.webp" 56 | - "*.wmv" 57 | - "*.woff" 58 | - "*.woff2" 59 | - "*.xz" 60 | - "*.Z" 61 | - "*.zip" 62 | full-files: 63 | - .git 64 | - .dockerignore 65 | - .env 66 | - .gitignore 67 | - .llm-context/ 68 | - CHANGELOG.md 69 | - Dockerfile 70 | - docker-compose.yml 71 | - elm-stuff 72 | - go.sum 73 | - LICENSE 74 | - package-lock.json 75 | - pnpm-lock.yaml 76 | - README.md 77 | - yarn.lock 78 | - "*.7z" 79 | - "*.app" 80 | - "*.bz2" 81 | - "*.cab" 82 | - "*.deb" 83 | - "*.dll" 84 | - "*.dmg" 85 | - "*.dylib" 86 | - "*.ear" 87 | - "*.eot" 88 | - "*.exe" 89 | - "*.gif" 90 | - "*.gz" 91 | - "*.icns" 92 | - "*.ico" 93 | - "*.iso" 94 | - "*.jar" 95 | - "*.jpeg" 96 | - "*.jpg" 97 | - "*.lock" 98 | - "*.log" 99 | - "*.lz" 100 | - "*.lzma" 101 | - "*.map" 102 | - "*.msi" 103 | - "*.pkg" 104 | - "*.png" 105 | - "*.rar" 106 | - "*.rpm" 107 | - "*.so" 108 | - "*.svg" 109 | - "*.tar" 110 | - "*.tar.bz2" 111 | - "*.tar.gz" 112 | - "*.tar.xz" 113 | - "*.tbz2" 114 | - "*.tgz" 115 | - "*.tif" 116 | - "*.tmp" 117 | - "*.ttf" 118 | - "*.txz" 119 | - "*.war" 120 | - "*.webp" 121 | - "*.woff" 122 | - "*.woff2" 123 | - "*.xz" 124 | - "*.Z" 125 | - "*.zip" 126 | excerpted-files: 127 | - .git 128 | - .dockerignore 129 | - .env 130 | - .gitignore 131 | - .llm-context/ 132 | - CHANGELOG.md 133 | - Dockerfile 134 | - docker-compose.yml 135 | - elm-stuff 136 | - go.sum 137 | - LICENSE 138 | - package-lock.json 139 | - pnpm-lock.yaml 140 | - README.md 141 | - yarn.lock 142 | - "*.7z" 143 | - "*.app" 144 | - "*.bz2" 145 | - "*.cab" 146 | - "*.deb" 147 | - "*.dll" 148 | - "*.dmg" 149 | - "*.dylib" 150 | - "*.ear" 151 | - "*.eot" 152 | - "*.exe" 153 | - "*.gif" 154 | - "*.gz" 155 | - "*.icns" 156 | - "*.ico" 157 | - "*.iso" 158 | - "*.jar" 159 | - "*.jpeg" 160 | - "*.jpg" 161 | - "*.lock" 162 | - "*.log" 163 | - "*.lz" 164 | - "*.lzma" 165 | - "*.map" 166 | - "*.msi" 167 | - "*.pkg" 168 | - "*.png" 169 | - "*.rar" 170 | - "*.rpm" 171 | - "*.so" 172 | - "*.svg" 173 | - "*.tar" 174 | - "*.tar.bz2" 175 | - "*.tar.gz" 176 | - "*.tar.xz" 177 | - "*.tbz2" 178 | - "*.tgz" 179 | - "*.tif" 180 | - "*.tmp" 181 | - "*.ttf" 182 | - "*.txz" 183 | - "*.war" 184 | - "*.webp" 185 | - "*.woff" 186 | - "*.woff2" 187 | - "*.xz" 188 | - "*.Z" 189 | - "*.zip" 190 | --- 191 | -------------------------------------------------------------------------------- /lib/openai_ex/error.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Error do 2 | defexception [ 3 | :status_code, 4 | :name, 5 | :message, 6 | :body, 7 | :code, 8 | :param, 9 | :type, 10 | :request_id, 11 | :request, 12 | :kind 13 | ] 14 | 15 | @error_names %{ 16 | bad_request: "BadRequestError", 17 | authentication: "AuthenticationError", 18 | permission_denied: "PermissionDeniedError", 19 | not_found: "NotFoundError", 20 | conflict: "ConflictError", 21 | unprocessable_entity: "UnprocessableEntityError", 22 | rate_limit: "RateLimitError", 23 | internal_server: "InternalServerError", 24 | api_response_validation: "APIResponseValidationError", 25 | api_connection: "APIConnectionError", 26 | api_timeout: "APITimeoutError", 27 | api_error: "APIError" 28 | } 29 | 30 | @status_code_map %{ 31 | 400 => :bad_request, 32 | 401 => :authentication, 33 | 403 => :permission_denied, 34 | 404 => :not_found, 35 | 409 => :conflict, 36 | 422 => :unprocessable_entity, 37 | 429 => :rate_limit 38 | } 39 | 40 | @impl true 41 | def exception(attrs) when is_list(attrs) do 42 | kind = attrs[:kind] || :api_error 43 | name = @error_names[kind] 44 | error = struct(__MODULE__, [name: name] ++ attrs) 45 | 46 | if is_map(error.body) do 47 | %{ 48 | error 49 | | code: error.body["code"], 50 | param: error.body["param"], 51 | type: error.body["type"] 52 | } 53 | else 54 | error 55 | end 56 | end 57 | 58 | @impl true 59 | def message(error = %__MODULE__{}) do 60 | "#{error.name}: #{error.message}" <> 61 | if(error.status_code, do: " (HTTP #{error.status_code})", else: "") <> 62 | if(error.body, do: " (JSON #{inspect(error.body)})", else: "") 63 | end 64 | 65 | def open_ai_error(message) do 66 | exception(kind: :open_ai_error, message: message) 67 | end 68 | 69 | def api_error(message, request, body) do 70 | exception(kind: :api_error, message: message, request: request, body: body) 71 | end 72 | 73 | def api_response_validation_error(response, body, message \\ nil) do 74 | exception( 75 | kind: :api_response_validation_error, 76 | message: message || "Data returned by API invalid for expected schema.", 77 | response: response, 78 | body: body, 79 | status_code: response.status 80 | ) 81 | end 82 | 83 | def api_status_error(message, response, body) do 84 | exception( 85 | kind: :api_status_error, 86 | message: message, 87 | response: response, 88 | body: body, 89 | status_code: response.status, 90 | request_id: get_in(response, [:headers, "x-request-id"]) 91 | ) 92 | end 93 | 94 | def api_connection_error(message, request) do 95 | exception(kind: :api_connection_error, message: message, request: request) 96 | end 97 | 98 | def api_timeout_error(request) do 99 | exception(kind: :api_timeout_error, message: "Request timed out.", request: request) 100 | end 101 | 102 | def status_error(status_code, response, body) when status_code in 400..499, 103 | do: status_error(@status_code_map[status_code], status_code, response, body) 104 | 105 | def status_error(status_code, response, body) when status_code in 500..599, 106 | do: status_error(:internal_server_error, 500, response, body) 107 | 108 | defp status_error(kind, status_code, response, body) when is_list(body) and length(body) > 0 do 109 | status_error(kind, status_code, response, List.first(body)) 110 | end 111 | 112 | defp status_error(kind, status_code, response, body) when is_map(body) do 113 | error = body["error"] 114 | 115 | exception( 116 | kind: kind, 117 | message: error["message"], 118 | response: response, 119 | body: error, 120 | status_code: status_code, 121 | request_id: get_in(response.headers, [Access.filter(&(elem(&1, 0) == "x-request-id")), Access.elem(1)]) 122 | ) 123 | end 124 | 125 | def sse_timeout_error() do 126 | exception(kind: :sse_timeout_error, message: "SSE next chunk timed out.") 127 | end 128 | 129 | def sse_user_cancellation() do 130 | exception(kind: :sse_cancellation, message: "SSE user canceled.") 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /notebooks/completions.livemd: -------------------------------------------------------------------------------- 1 | # Completions Bot (Deprecated API) 2 | 3 | ```elixir 4 | Mix.install([ 5 | {:openai_ex, "~> 0.9.18"}, 6 | {:kino, "~> 0.17.0"} 7 | ]) 8 | 9 | alias OpenaiEx 10 | alias OpenaiEx.Completion 11 | ``` 12 | 13 | ## Deprecation Notice 14 | 15 | Note that OpenAI has deprecated the models that use the Completion API. The newer models all use the Chat Completion API. As such, this livebook will **stop working** when the models are discontinued. 16 | 17 | It is still being left in the documentation in case people want to use it with non OpenAI models through an OpenAI API proxy. 18 | 19 | ## Model Choices 20 | 21 | ```elixir 22 | openai = 23 | System.fetch_env!("LB_OPENAI_API_KEY") 24 | |> OpenaiEx.new() 25 | 26 | # uncomment the line at the end of this block comment when working with a local LLM with a 27 | # proxy such as llama.cpp-python in the example below, our development livebook server is 28 | # running in a docker dev container while the local llm is running on the host machine 29 | # |> OpenaiEx.with_base_url("http://host.docker.internal:8000/v1") 30 | ``` 31 | 32 | ```elixir 33 | comp_models = [ 34 | "text-davinci-003", 35 | "text-davinci-002", 36 | "text-curie-001", 37 | "text-babbage-001", 38 | "text-ada-001" 39 | ] 40 | ``` 41 | 42 | ## Normal Completion 43 | 44 | This function calls the completion API and renders the result in the given Kino frame. 45 | 46 | ```elixir 47 | completion = fn model, prompt, max_tokens, temperature, last_frame -> 48 | text = 49 | openai 50 | |> Completion.create!(%{ 51 | model: model, 52 | prompt: prompt, 53 | max_tokens: max_tokens, 54 | temperature: temperature 55 | }) 56 | |> Map.get("choices") 57 | |> Enum.at(0) 58 | |> Map.get("text") 59 | 60 | Kino.Frame.render(last_frame, Kino.Markdown.new("**Bot** #{text}")) 61 | text 62 | end 63 | ``` 64 | 65 | ## Streaming Completion 66 | 67 | This function calls the streaming completion API and continuously updates the Kino frame with the latest tokens 68 | 69 | ```elixir 70 | completion_stream = fn model, prompt, max_tokens, temperature, last_frame -> 71 | stream = 72 | openai 73 | |> Completion.create!( 74 | %{ 75 | model: model, 76 | prompt: prompt, 77 | max_tokens: max_tokens, 78 | temperature: temperature 79 | }, 80 | stream: true 81 | ) 82 | 83 | token_stream = 84 | stream.body_stream 85 | |> Stream.flat_map(& &1) 86 | |> Stream.map(fn %{data: data} -> 87 | data |> Map.get("choices") |> Enum.at(0) |> Map.get("text") 88 | end) 89 | 90 | token_stream 91 | |> Enum.reduce("", fn out, acc -> 92 | next = acc <> out 93 | Kino.Frame.render(last_frame, Kino.Markdown.new("**Bot** #{next}")) 94 | next 95 | end) 96 | end 97 | ``` 98 | 99 | ## Create a Form UI 100 | 101 | This is a function to create a Form UI that can be used to call the completion API. The 2nd parameter determines whether the normal or streaming API is called. 102 | 103 | ```elixir 104 | create_form = fn title, completion_fn -> 105 | chat_frame = Kino.Frame.new() 106 | last_frame = Kino.Frame.new() 107 | 108 | Kino.Frame.render(chat_frame, Kino.Markdown.new(title)) 109 | 110 | inputs = [ 111 | model: Kino.Input.select("Model", comp_models |> Enum.map(fn x -> {x, x} end)), 112 | max_tokens: Kino.Input.number("Max Tokens", default: 400), 113 | temperature: Kino.Input.number("Temperature", default: 1), 114 | prompt: Kino.Input.textarea("Prompt") 115 | ] 116 | 117 | form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:prompt]) 118 | 119 | Kino.listen( 120 | form, 121 | fn %{ 122 | data: %{ 123 | prompt: prompt, 124 | model: model, 125 | max_tokens: max_tokens, 126 | temperature: temperature 127 | } 128 | } -> 129 | Kino.Frame.render(chat_frame, Kino.Markdown.new(title)) 130 | Kino.Frame.append(chat_frame, Kino.Markdown.new("**Me** #{prompt}")) 131 | 132 | completion_fn.(model, prompt, max_tokens, temperature, last_frame) 133 | end 134 | ) 135 | 136 | Kino.Layout.grid([chat_frame, last_frame, form], boxed: true, gap: 16) 137 | end 138 | ``` 139 | 140 | ### Normal Chatbot 141 | 142 | Create the Form for the non-streaming completion API and use it. 143 | 144 | ```elixir 145 | create_form.("## Completion Chatbot", completion) 146 | ``` 147 | 148 | ### Streaming Chatbot 149 | 150 | Create the form for the streaming completion API, and use it. 151 | 152 | ```elixir 153 | create_form.("## Streaming Chatbot", completion_stream) 154 | ``` -------------------------------------------------------------------------------- /lib/openai_ex/http_finch.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.HttpFinch do 2 | @moduledoc false 3 | require Logger 4 | alias OpenaiEx.Error 5 | 6 | def get(openai = %OpenaiEx{}, url) do 7 | build_req(:get, openai, url) |> request(openai) 8 | end 9 | 10 | def delete(openai = %OpenaiEx{}, url) do 11 | build_req(:delete, openai, url) |> request(openai) 12 | end 13 | 14 | def post(openai = %OpenaiEx{}, url, multipart: multipart) do 15 | build_multipart(openai, url, multipart) |> request(openai) 16 | end 17 | 18 | def post(openai = %OpenaiEx{}, url, json: json) do 19 | build_post(openai, url, json: json) |> request(openai) 20 | end 21 | 22 | def request_options(openai = %OpenaiEx{}) do 23 | [receive_timeout: openai |> Map.get(:receive_timeout)] 24 | end 25 | 26 | defp headers(openai = %OpenaiEx{}) do 27 | openai._http_headers 28 | end 29 | 30 | def stream(request, openai = %OpenaiEx{}, fun) do 31 | Finch.stream(request, Map.get(openai, :finch_name), nil, fun, request_options(openai)) 32 | end 33 | 34 | def request(request, openai = %OpenaiEx{}) do 35 | case Finch.request(request, Map.get(openai, :finch_name), request_options(openai)) do 36 | {:ok, response} -> to_response(response) 37 | {:error, error} -> to_error(error, request) 38 | end 39 | end 40 | 41 | defp build_req(method, openai = %OpenaiEx{}, url) do 42 | Finch.build(method, openai.base_url <> url, headers(openai)) 43 | end 44 | 45 | def build_post(openai = %OpenaiEx{}, url, json: json) do 46 | Finch.build( 47 | :post, 48 | openai.base_url <> url, 49 | headers(openai) ++ [{"Content-Type", "application/json"}], 50 | Jason.encode_to_iodata!(json) 51 | ) 52 | end 53 | 54 | def build_multipart(openai = %OpenaiEx{}, url, multipart) do 55 | Finch.build( 56 | :post, 57 | openai.base_url <> url, 58 | headers(openai) ++ 59 | [ 60 | {"Content-Type", Multipart.content_type(multipart, "multipart/form-data")}, 61 | {"Content-Length", to_string(Multipart.content_length(multipart))} 62 | ], 63 | {:stream, Multipart.body_stream(multipart)} 64 | ) 65 | end 66 | 67 | defp to_response(%Finch.Response{ 68 | status: status, 69 | headers: headers, 70 | body: body, 71 | trailers: trailers 72 | }) 73 | when status in 200..299 do 74 | {:ok, %{status: status, headers: headers, body: body, trailers: trailers}} 75 | end 76 | 77 | defp to_response(r = %Finch.Response{status: status, body: body}) 78 | when is_integer(status) do 79 | decoded_body = 80 | with {:ok, json} <- Jason.decode(body), 81 | do: json, 82 | else: ({:error, _} -> %{"error" => %{"message" => "#{inspect(body)}"}}) 83 | 84 | {:error, Error.status_error(status, r, decoded_body)} 85 | end 86 | 87 | def to_error!(error, request) do 88 | case to_error(error, request) do 89 | {:error, exception} -> exception 90 | end 91 | end 92 | 93 | def to_error(:timeout, request), do: {:error, Error.api_timeout_error(request)} 94 | 95 | def to_error(:closed, request), 96 | do: {:error, Error.api_connection_error("Connection closed.", request)} 97 | 98 | def to_error(:nxdomain, request), 99 | do: {:error, Error.api_connection_error("Bad address - Non-Existent Domain.", request)} 100 | 101 | def to_error(%{reason: reason}, request), do: to_error(reason, request) 102 | 103 | def to_error(error, request) when is_exception(error) do 104 | sanitized = if request, do: sanitize_request(request), else: nil 105 | Logger.warning("Unhandled Finch exception: #{inspect(error)}, request: #{inspect(sanitized)}") 106 | {:error, Error.api_connection_error(Exception.message(error), sanitized)} 107 | end 108 | 109 | def to_error(error, request) do 110 | sanitized = if request, do: sanitize_request(request), else: nil 111 | Logger.warning("Unhandled Finch error: #{inspect(error)}, request #{inspect(sanitized)}") 112 | {:error, Error.api_connection_error(error, sanitized)} 113 | end 114 | 115 | def sanitize_request(%Finch.Request{headers: headers} = request) when is_list(headers) do 116 | sanitized_headers = 117 | Enum.map(headers, fn 118 | {key, value} when is_binary(key) -> 119 | if String.downcase(key) == "authorization" and String.starts_with?(value, "Bearer ") do 120 | {key, "Bearer [REDACTED]"} 121 | else 122 | {key, value} 123 | end 124 | 125 | other -> 126 | other 127 | end) 128 | 129 | %{request | headers: sanitized_headers} 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /.llm-context/templates/lc/overview.j2: -------------------------------------------------------------------------------- 1 | # {% if tools_available %}Repository{% else %}Detailed Repository{% endif %} Content: **{{ project_name }}** 2 | 3 | ## Instructions for AI: {% if overview_mode == "focused" %}⚠️ FOCUSED PROJECT CONTEXT PROVIDED - {% if tools_available %}USE TOOLS FOR ADDITIONAL FILES{% else %}USE COMMANDS FOR ADDITIONAL FILES{% endif %} ⚠️{% else %}⚠️ COMPLETE PROJECT CONTEXT PROVIDED - NO NEED TO REQUEST ADDITIONAL CONTEXT ⚠️{% endif %} 4 | 5 | ## Quick Reference 6 | - ✓ = Full content included below 7 | {% if excerpts -%} 8 | - O = Outlined content (definitions/functions only) 9 | - E = Excerpted content (specific sections extracted) 10 | {% endif -%} 11 | {% if sample_excluded_files -%} 12 | - ✗ = Excluded (not included) 13 | {% endif %} 14 | 15 | > Generation timestamp: {{ context_timestamp }} 16 | > {% if tools_available %}For updates: Use lc-changed to identify changes, then lc-missing for specific files{% else %}For updates: Use the lc-missing commands shown below{% endif %} 17 | 18 | {% if overview_mode == "focused" -%} 19 | This context presents a focused view of the _/{{ project_name }}_ repository, including complete contents for key files and excerpted content for important code sections. {% if tools_available %}Additional files can be retrieved using the lc-missing tool.{% else %}Additional files can be requested using the lc-missing commands shown below.{% endif %} 20 | {%- else -%} 21 | This context presents a comprehensive view of the _/{{ project_name }}_ repository. 22 | {%- endif %} 23 | 24 | {% if sample_requested_files or sample_excluded_files %} 25 | ## Instructions for AI: 📂 Before {% if tools_available %}Accessing{% else %}Requesting{% endif %} Any Files 26 | 27 | 1. **SEARCH THIS DOCUMENT** to check if the file is already included below 28 | 2. **CHECK the repository structure** below to confirm file status (✓,{% if excerpts %} O, E,{% endif %} or ✗) 29 | 3. {% if overview_mode == "focused" %}{% if tools_available %}Use lc-missing for ✗{% if excerpts %}, O, or E{% endif %} files that are needed for your analysis{% else %}Use lc-missing commands below for ✗{% if excerpts %}, O, or E{% endif %} files that are needed{% endif %}{% else %}Only request ✗{% if excerpts %}, O, or E{% endif %} files that are absolutely necessary for your analysis{% endif %} 30 | 4. **Never assume file contents** - if a file is ✗, O, or E and you need it to answer accurately, use lc_missing to fetch it first. Don't guess at the contents based on conventions / patterns / partial information, etc. 31 | 32 | {% if tools_available %} 33 | {% if overview_mode == "focused" -%} 34 | Use lc-missing for: 35 | - Any ✗{% if excerpts %}, O, or E{% endif %} files you need to examine 36 | - Files modified since context generation (use lc-changed to identify these) 37 | {%- else -%} 38 | Only use lc-missing for: 39 | - Files modified since context generation (use lc-changed to identify these) 40 | - Files marked ✗{% if excerpts %}, O, or E{% endif %} in the repository structure that you need to examine 41 | {%- endif %} 42 | 43 | {% if excerpts %} 44 | **EFFICIENCY TIP:** 45 | - For O files: Use lc-missing with param_type "i" to get specific implementations 46 | - For E files: Use lc-missing with param_type "e" to get excluded sections 47 | - For complete files: Use lc-missing with param_type "f" 48 | {% endif %} 49 | {% endif %} 50 | 51 | ### How to Request Missing Files 52 | 53 | {% if tools_available %} 54 | Using the lc-missing tool: 55 | **ROOT PATH must be: {{ abs_root_path }}** 56 | 57 | For files: 58 | ```json 59 | { 60 | "root_path": "{{ abs_root_path }}", 61 | "param_type": "f", 62 | "data": {% if sample_excluded_files %}{{ sample_excluded_files | tojson }}{% else %}{{ sample_requested_files | tojson }}{% endif %}, 63 | "timestamp": {{ context_timestamp }} 64 | } 65 | ``` 66 | 67 | For modified files since context generation: 68 | ```json 69 | { 70 | "root_path": "{{ abs_root_path }}", 71 | "rule_name": "lc/prm-developer", 72 | "timestamp": {{ context_timestamp }} 73 | } 74 | ``` 75 | {% else %} 76 | For files, the user should run: 77 | ```bash 78 | lc-missing -f {% if sample_excluded_files %}{{ sample_excluded_files | tojson | tojson }}{% else %}{{ sample_requested_files | tojson | tojson }}{% endif %} -t {{ context_timestamp }} 79 | ``` 80 | 81 | For modified files since context generation, the user should run: 82 | ```bash 83 | lc-changed "{{ abs_root_path }}" lc/prm-developer {{ context_timestamp }} 84 | ``` 85 | {% endif %} 86 | {% endif %} 87 | 88 | ## Repository Structure{% if overview_mode == "focused" %} (Focused View){% endif %} 89 | 90 | {% if overview_mode == "focused" -%} 91 | This focused view shows complete file details for directories containing included files, and folder summaries for directories with only excluded files. 92 | {%- else -%} 93 | ``` 94 | Status: ✓=Full content, O=Outlined content, E=Excerpted content, ✗=Excluded 95 | Format: status path bytes (size) age 96 | ``` 97 | {%- endif %} 98 | 99 | ``` 100 | {{ overview }} 101 | ``` 102 | -------------------------------------------------------------------------------- /lib/openai_ex.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx do 2 | @moduledoc """ 3 | `OpenaiEx` is an Elixir library that provides a community-maintained client for 4 | the OpenAI API. 5 | 6 | The library closely follows the structure of the [official OpenAI API client libraries](https://platform.openai.com/docs/api-reference) 7 | for [Python](https://github.com/openai/openai-python) making it easy to understand 8 | and reuse existing documentation and code. 9 | """ 10 | defstruct token: nil, 11 | organization: nil, 12 | project: nil, 13 | beta: nil, 14 | base_url: "https://api.openai.com/v1", 15 | receive_timeout: 15_000, 16 | stream_timeout: :infinity, 17 | finch_name: OpenaiEx.Finch, 18 | _ep_path_mapping: &OpenaiEx._identity/1, 19 | _http_headers: nil 20 | 21 | @doc """ 22 | Creates a new OpenaiEx struct with the specified token and organization. 23 | 24 | See https://platform.openai.com/docs/api-reference/authentication for details. 25 | """ 26 | def new(token, organization \\ nil, project \\ nil) do 27 | headers = 28 | [{"Authorization", "Bearer #{token}"}] ++ 29 | if(is_nil(organization), 30 | do: [], 31 | else: [{"OpenAI-Organization", organization}] 32 | ) ++ 33 | if(is_nil(project), 34 | do: [], 35 | else: [{"OpenAI-Project", project}] 36 | ) 37 | 38 | %OpenaiEx{ 39 | token: token, 40 | organization: organization, 41 | project: project, 42 | _http_headers: headers 43 | } 44 | end 45 | 46 | @doc """ 47 | Create file parameter struct for use in multipart requests. 48 | 49 | OpenAI API has endpoints which need a file parameter, such as Files and Audio. 50 | This function creates a file parameter given a name (optional) and content or a local file path. 51 | """ 52 | def new_file(name: name, content: content) do 53 | {name, content} 54 | end 55 | 56 | def new_file(path: path) do 57 | {path} 58 | end 59 | 60 | # Globals for internal library use, **not** for public use. 61 | 62 | @assistants_beta_string "assistants=v2" 63 | @doc false 64 | def with_assistants_beta(openai = %OpenaiEx{}) do 65 | openai 66 | |> Map.put(:beta, @assistants_beta_string) 67 | |> Map.get_and_update(:_http_headers, fn headers -> 68 | {headers, headers ++ [{"OpenAI-Beta", @assistants_beta_string}]} 69 | end) 70 | |> elem(1) 71 | end 72 | 73 | # Globals to allow slight changes to API 74 | # Not public, and with no guarantee that they will continue to be supported. 75 | 76 | @doc false 77 | def _identity(x), do: x 78 | 79 | @doc false 80 | def _with_ep_path_mapping(openai = %OpenaiEx{}, ep_path_mapping) do 81 | openai |> Map.put(:_ep_path_mapping, ep_path_mapping) 82 | end 83 | 84 | # https://learn.microsoft.com/en-us/azure/ai-services/openai/reference 85 | @doc false 86 | def _azure_ep_path_mapping(api_version) do 87 | fn ep -> 88 | case ep do 89 | "/chat/completions" -> "/chat/completions?api-version=#{api_version}" 90 | "/completions" -> "/completions?api-version=#{api_version}" 91 | "/embeddings" -> "/embeddings?api-version=#{api_version}" 92 | _ -> ep 93 | end 94 | end 95 | end 96 | 97 | # Azure OpenAI. Not public and with no guarantee of continued support. 98 | def _for_azure(openai = %OpenaiEx{}, resource_name, deployment_id, api_version) do 99 | openai 100 | |> with_base_url("https://#{resource_name}.openai.azure.com/openai/deployments/#{deployment_id}") 101 | |> _with_ep_path_mapping(_azure_ep_path_mapping(api_version)) 102 | end 103 | 104 | def _for_azure(azure_api_key, resource_name, deployment_id, api_version) do 105 | %OpenaiEx{ 106 | _http_headers: [{"api-key", "#{azure_api_key}"}] 107 | } 108 | |> _for_azure(resource_name, deployment_id, api_version) 109 | end 110 | 111 | # Globals for public use. 112 | 113 | def with_base_url(openai = %OpenaiEx{}, base_url) do 114 | openai |> Map.put(:base_url, base_url) 115 | end 116 | 117 | def with_additional_headers(openai = %OpenaiEx{}, additional_headers) do 118 | Map.update(openai, :_http_headers, [], fn existing_headers -> 119 | existing_headers ++ Enum.to_list(additional_headers) 120 | end) 121 | end 122 | 123 | def with_receive_timeout(openai = %OpenaiEx{}, timeout) 124 | when is_integer(timeout) and timeout > 0 do 125 | openai |> Map.put(:receive_timeout, timeout) 126 | end 127 | 128 | def with_stream_timeout(openai = %OpenaiEx{}, timeout) 129 | when is_integer(timeout) and timeout > 0 do 130 | openai |> Map.put(:stream_timeout, timeout) 131 | end 132 | 133 | def with_finch_name(openai = %OpenaiEx{}, finch_name) do 134 | openai |> Map.put(:finch_name, finch_name) 135 | end 136 | 137 | @doc false 138 | def list_query_fields() do 139 | [ 140 | :after, 141 | :before, 142 | :limit, 143 | :order 144 | ] 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/openai_ex/container_files.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.ContainerFiles do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI Container Files API. The API reference can be found at https://platform.openai.com/docs/api-reference/container-files. 4 | """ 5 | 6 | alias OpenaiEx.Http 7 | 8 | defp ep_url(container_id, file_id \\ nil, action \\ nil) do 9 | base = "/containers/#{container_id}/files" 10 | 11 | base 12 | |> append_file_id(file_id) 13 | |> append_action(action) 14 | end 15 | 16 | defp append_file_id(url, nil), do: url 17 | defp append_file_id(url, file_id), do: "#{url}/#{file_id}" 18 | 19 | defp append_action(url, nil), do: url 20 | defp append_action(url, action), do: "#{url}/#{action}" 21 | 22 | @doc """ 23 | Creates a new file upload request with the given arguments. 24 | 25 | ## Examples 26 | 27 | iex> OpenaiEx.ContainerFiles.new_upload(file: {"test.txt", "content"}) 28 | %{file: {"test.txt", "content"}} 29 | 30 | iex> OpenaiEx.ContainerFiles.new_upload(file: {"/path/to/file.txt"}) 31 | %{file: {"/path/to/file.txt"}} 32 | """ 33 | def new_upload(args) when is_list(args) do 34 | args |> Enum.into(%{}) |> new_upload() 35 | end 36 | 37 | def new_upload(args) when is_map(args) do 38 | args |> Map.take([:file]) |> validate_upload_request() 39 | end 40 | 41 | @doc """ 42 | Creates a new file reference request with the given arguments. 43 | 44 | Example usage: 45 | 46 | iex> OpenaiEx.ContainerFiles.new_reference(file_id: "file-123") 47 | %{file_id: "file-123"} 48 | """ 49 | def new_reference(args) when is_list(args) do 50 | args |> Enum.into(%{}) |> new_reference() 51 | end 52 | 53 | def new_reference(args) when is_map(args) do 54 | args |> Map.take([:file_id]) |> validate_reference_request() 55 | end 56 | 57 | defp validate_upload_request(request) do 58 | if Map.has_key?(request, :file) do 59 | request 60 | else 61 | raise ArgumentError, "Upload request must include :file" 62 | end 63 | end 64 | 65 | defp validate_reference_request(request) do 66 | if Map.has_key?(request, :file_id) do 67 | request 68 | else 69 | raise ArgumentError, "Reference request must include :file_id" 70 | end 71 | end 72 | 73 | @doc """ 74 | Lists all files in a container. 75 | 76 | See https://platform.openai.com/docs/api-reference/container-files/listContainerFiles 77 | """ 78 | def list!(openai = %OpenaiEx{}, container_id, params \\ %{}) do 79 | openai |> list(container_id, params) |> Http.bang_it!() 80 | end 81 | 82 | def list(openai = %OpenaiEx{}, container_id, params \\ %{}) do 83 | query_params = params |> Map.take(OpenaiEx.list_query_fields()) 84 | openai |> Http.get(ep_url(container_id), query_params) 85 | end 86 | 87 | @doc """ 88 | Creates a new container file via upload or file reference. 89 | 90 | See https://platform.openai.com/docs/api-reference/container-files/createContainerFile 91 | """ 92 | def create!(openai = %OpenaiEx{}, container_id, request) do 93 | openai |> create(container_id, request) |> Http.bang_it!() 94 | end 95 | 96 | def create(openai = %OpenaiEx{}, container_id, request) do 97 | cond do 98 | Map.has_key?(request, :file) -> 99 | # Handle file upload with multipart 100 | multipart = request |> Http.to_multi_part_form_data([:file]) 101 | openai |> Http.post(ep_url(container_id), multipart: multipart) 102 | 103 | Map.has_key?(request, :file_id) -> 104 | # Handle file reference with JSON 105 | openai |> Http.post(ep_url(container_id), json: request) 106 | 107 | true -> 108 | {:error, "Request must include either :file or :file_id"} 109 | end 110 | end 111 | 112 | @doc """ 113 | Retrieves a specific container file by ID. 114 | 115 | See https://platform.openai.com/docs/api-reference/container-files/retrieveContainerFile 116 | """ 117 | def retrieve!(openai = %OpenaiEx{}, container_id, file_id) do 118 | openai |> retrieve(container_id, file_id) |> Http.bang_it!() 119 | end 120 | 121 | def retrieve(openai = %OpenaiEx{}, container_id, file_id) do 122 | openai |> Http.get(ep_url(container_id, file_id)) 123 | end 124 | 125 | @doc """ 126 | Retrieves the content of a container file. 127 | 128 | See https://platform.openai.com/docs/api-reference/container-files/retrieveContainerFileContent 129 | """ 130 | def content!(openai = %OpenaiEx{}, container_id, file_id) do 131 | openai |> content(container_id, file_id) |> Http.bang_it!() 132 | end 133 | 134 | def content(openai = %OpenaiEx{}, container_id, file_id) do 135 | openai |> Http.get_no_decode(ep_url(container_id, file_id, "content")) 136 | end 137 | 138 | @doc """ 139 | Deletes a container file. 140 | 141 | See https://platform.openai.com/docs/api-reference/container-files/deleteContainerFile 142 | """ 143 | def delete!(openai = %OpenaiEx{}, container_id, file_id) do 144 | openai |> delete(container_id, file_id) |> Http.bang_it!() 145 | end 146 | 147 | def delete(openai = %OpenaiEx{}, container_id, file_id) do 148 | openai |> Http.delete(ep_url(container_id, file_id)) 149 | end 150 | 151 | @doc false 152 | def file_fields() do 153 | [:file] 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | > Note: This changelog was retroactively created (by Claude 3.7 Sonnet using the git MCP plugin) in March 2025 and covers versions from 0.8.0 forward. 8 | > Earlier versions do not have detailed change records. 9 | 10 | ## [0.9.18] - 2025-10-12 11 | 12 | ### Feat 13 | - added conversation param to responses endpoint (d6aa8fb) 14 | 15 | ## [0.9.17] - 2025-09-29 16 | 17 | ### Feat 18 | - added Conversations endpoints (ea82703) 19 | 20 | ## [0.9.16] - 2025-09-11 21 | 22 | ### Fix 23 | - fixed error logging on raise (67e12b7) 24 | 25 | ## [0.9.15] - 2025-09-11 26 | 27 | ### Feat 28 | - added Evals endpoints (67e1df0) 29 | - added stable VectorStores endpoint (2ce8408) 30 | 31 | ## [0.9.14] - 2025-08-21 32 | 33 | ### Feat 34 | - added additional request parameter fields to Responses API (859158d) 35 | 36 | ## [0.9.13] - 2025-07-16 37 | 38 | ### BREAKING CHANGE: Stream errors now return OpenaiEx.Error structs instead of raw exceptions 39 | 40 | ### Feat 41 | - added Containers and ContainerFiles APIs (49ec7c2) 42 | 43 | ### Refactor 44 | - standardize error handling to return OpenaiEx.Error 45 | 46 | ### Build 47 | - Updated `finch` from 0.19.0 to 0.20.0 48 | 49 | ## [0.9.12] - 2025-06-03 50 | 51 | ### Fix 52 | - fix error masking during streaming (356769b) 53 | 54 | ## [0.9.11] - 2025-06-03 55 | 56 | ### Fix 57 | - use api_timeout_error for SSE initial connection timeouts (6ec7d4c) 58 | - updated image edit example with (now required) dummy filenames (6ec7d4c) 59 | 60 | ## [0.9.10] - 2025-05-15 61 | 62 | ### Fix 63 | - handle non-JSON payloads in 3XX/4XX responses gracefully (4d7ca49) 64 | 65 | ## [0.9.9] - 2025-05-15 66 | 67 | ### Feat 68 | - handle list-formatted errors from Gemini API (e1669f8) 69 | 70 | ## [0.9.8] - 2025-05-15 71 | 72 | ### Fixed 73 | - handle non-JSON responses from 5xx errors (895666f) 74 | 75 | ## [0.9.7] - 2025-04-26 76 | 77 | ### Added 78 | - update/sort image endpoint params (7fae56c) 79 | - add support for multiple input images (e4f2489) 80 | 81 | ## [0.9.6] - 2025-04-25 82 | 83 | ### Fixed 84 | - add new 4o image gen api fields (f2d3d97) 85 | 86 | ## [0.9.5] - 2025-04-14 87 | 88 | ### Fixed 89 | - Fixed resolve streaming request hang during timeout (867477a) 90 | 91 | ## [0.9.4] - 2025-03-26 92 | 93 | ### Added 94 | - Added retroactive changelog documenting project history since version 0.8.0 (5c0d501) 95 | 96 | ### Fixed 97 | - Fixed potential race condition in streaming implementation (42aa402) 98 | 99 | ## [0.9.3] - 2025-03-25 100 | 101 | ### Fixed 102 | - Cleanup streaming task in error path (6370ff6) 103 | - Clean up streaming tasks to prevent unexpected messages (634f5f5) 104 | - Remove unused task param (81b6812) 105 | 106 | ## [0.9.2] - 2025-03-21 107 | 108 | ### Added 109 | - Support for handling array parameters properly in query string generation (b7e9aff) 110 | - Added query parameter to "new" functions (ab38cf2) 111 | - Added 'include' to API fields for response creation (911964d) 112 | - Added list/map query parameter to endpoints (a9aaca6) 113 | 114 | ### Fixed 115 | - Proper handling of list output (10ab65b) 116 | - Remove extra list construction (eb72366) 117 | 118 | ### Changed 119 | - Updated source URL (5c2631c) 120 | 121 | ## [0.9.1] - 2025-03-18 122 | 123 | ### Added 124 | - CRUD operations for Chat Completions and Responses (b878dbd) 125 | 126 | ### Fixed 127 | - Flatten query metadata (bbe5719) 128 | 129 | ## [0.9.0] - 2025-03-12 130 | 131 | ### Added 132 | - Responses API endpoint (99ecdec) 133 | - Web-search parameter to chat completions (08fbd03) 134 | 135 | ### Changed 136 | - Updated dependencies (853f562) 137 | - Removed non-executable docs leaving only links to the API reference (57ae9cc) 138 | 139 | ### Fixed 140 | - Removed redundant alias (9418f81) 141 | 142 | ## [0.8.6] - 2025-02-06 143 | 144 | ### Added 145 | - Redact API token in Finch error logs (4499c77) 146 | 147 | ### Changed 148 | - Updated dependencies (93a6e4e) 149 | - Improved completion documentation (ce95487) 150 | 151 | ## [0.8.5] - 2024-12-24 152 | 153 | ### Added 154 | - Support for developer messages (3b6b11e) 155 | - Support for reasoning models (0390d67) 156 | 157 | ### Changed 158 | - Updated dependencies (bb0d306) 159 | 160 | ## [0.8.4] - 2024-12-13 161 | 162 | ### Added 163 | - Store parameter to Chat Completion API call (64741d5) 164 | 165 | ## [0.8.3] - 2024-10-19 166 | 167 | ### Added 168 | - Allow arbitrary HTTP headers to be added to requests (7f20ed7) 169 | 170 | ## [0.8.2] - 2024-10-11 171 | 172 | ### Added 173 | - Added option to provide an OpenAI project ID (dfda65c) 174 | - Handle :nxdomain errors (fcd591f) 175 | 176 | ### Changed 177 | - Updated dependencies (487554a) 178 | - Fixed credo warnings (e45b158) 179 | 180 | ## [0.8.1] - 2024-09-04 181 | 182 | ### Added 183 | - Restored completions API (e538b8a) 184 | - Restored completions notebook (2d2c1ba) 185 | 186 | ### Changed 187 | - Updated dependencies 188 | 189 | ## [0.8.0] - 2024-07-29 190 | 191 | ### Added 192 | - Logging for unknown errors (80e6f13) 193 | 194 | ### Fixed 195 | - Added error handler for closed connections (a03bdc1) 196 | 197 | ### Changed 198 | - Updated dependencies (86df1c4) 199 | 200 | ## [0.7.0] - 2024-06-30 201 | -------------------------------------------------------------------------------- /lib/openai_ex/chat_completions.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Chat.Completions do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI chat completions API. The API reference can be found at https://platform.openai.com/docs/api-reference/chat/completions. 4 | """ 5 | alias OpenaiEx.{Http, HttpSse} 6 | 7 | @api_fields [ 8 | :messages, 9 | :model, 10 | :audio, 11 | :frequency_penalty, 12 | :logit_bias, 13 | :logprobs, 14 | :max_completion_tokens, 15 | :max_tokens, 16 | :metadata, 17 | :modalities, 18 | :n, 19 | :parallel_tool_calls, 20 | :prediction, 21 | :presence_penalty, 22 | :reasoning_effort, 23 | :response_format, 24 | :seed, 25 | :service_tier, 26 | :stop, 27 | :store, 28 | :stream_options, 29 | :temperature, 30 | :top_p, 31 | :top_logprobs, 32 | :tools, 33 | :tool_choice, 34 | :user, 35 | :web_search_options 36 | ] 37 | 38 | @doc """ 39 | Creates a new chat completion request 40 | 41 | Example usage: 42 | 43 | iex> _request = OpenaiEx.Chat.Completions.new(model: "davinci", messages: [OpenaiEx.ChatMessage.user("Hello, world!")]) 44 | %{messages: [%{content: "Hello, world!", role: "user"}], model: "davinci"} 45 | 46 | iex> _request = OpenaiEx.Chat.Completions.new(%{model: "davinci", messages: [OpenaiEx.ChatMessage.user("Hello, world!")]}) 47 | %{messages: [%{content: "Hello, world!", role: "user"}], model: "davinci"} 48 | """ 49 | 50 | def new(args = [_ | _]) do 51 | args |> Enum.into(%{}) |> new() 52 | end 53 | 54 | def new(args = %{model: _, messages: _}) do 55 | args |> Map.take(@api_fields) 56 | end 57 | 58 | @ep_url "/chat/completions" 59 | 60 | defp ep_url(completion_id \\ nil, action \\ nil) do 61 | @ep_url <> 62 | if(is_nil(completion_id), do: "", else: "/#{completion_id}") <> 63 | if(is_nil(action), do: "", else: "/#{action}") 64 | end 65 | 66 | @doc """ 67 | Calls the chat completion 'create' endpoint. 68 | 69 | See https://platform.openai.com/docs/api-reference/chat/completions/create for more information. 70 | """ 71 | def create!(openai = %OpenaiEx{}, chat_completion = %{}, stream: true) do 72 | openai |> create(chat_completion, stream: true) |> Http.bang_it!() 73 | end 74 | 75 | def create(openai = %OpenaiEx{}, chat_completion = %{}, stream: true) do 76 | ep = Map.get(openai, :_ep_path_mapping).(@ep_url) 77 | 78 | openai 79 | |> HttpSse.post(ep, json: chat_completion |> Map.take(@api_fields) |> Map.put(:stream, true)) 80 | end 81 | 82 | def create!(openai = %OpenaiEx{}, chat_completion = %{}) do 83 | openai |> create(chat_completion) |> Http.bang_it!() 84 | end 85 | 86 | def create(openai = %OpenaiEx{}, chat_completion = %{}) do 87 | ep = Map.get(openai, :_ep_path_mapping).(@ep_url) 88 | openai |> Http.post(ep, json: chat_completion |> Map.take(@api_fields)) 89 | end 90 | 91 | @doc """ 92 | Retrieves a stored chat completion by ID. 93 | 94 | See https://platform.openai.com/docs/api-reference/chat/get 95 | """ 96 | def retrieve!(openai = %OpenaiEx{}, completion_id: completion_id) do 97 | openai |> retrieve(completion_id: completion_id) |> Http.bang_it!() 98 | end 99 | 100 | def retrieve(openai = %OpenaiEx{}, completion_id: completion_id) do 101 | openai |> Http.get(ep_url(completion_id)) 102 | end 103 | 104 | @doc """ 105 | Lists messages from a stored chat completion. 106 | 107 | See https://platform.openai.com/docs/api-reference/chat/getMessages 108 | """ 109 | def messages_list!(openai = %OpenaiEx{}, completion_id, opts \\ []) do 110 | openai |> messages_list(completion_id, opts) |> Http.bang_it!() 111 | end 112 | 113 | def messages_list(openai = %OpenaiEx{}, completion_id, opts \\ []) do 114 | params = opts |> Enum.into(%{}) |> Map.take(OpenaiEx.list_query_fields()) 115 | openai |> Http.get(ep_url(completion_id, "messages"), params) 116 | end 117 | 118 | @doc """ 119 | Lists stored Chat Completions. 120 | 121 | See https://platform.openai.com/docs/api-reference/chat/list 122 | """ 123 | def list!(openai = %OpenaiEx{}, opts \\ []) do 124 | openai |> list(opts) |> Http.bang_it!() 125 | end 126 | 127 | def list(openai = %OpenaiEx{}, opts \\ []) do 128 | p = opts |> Enum.into(%{}) |> Map.take(OpenaiEx.list_query_fields() ++ [:metadata, :model]) 129 | openai |> Http.get(ep_url(), p) 130 | end 131 | 132 | @doc """ 133 | Updates a stored chat completion. 134 | 135 | See https://platform.openai.com/docs/api-reference/chat/update 136 | """ 137 | def update!(openai = %OpenaiEx{}, completion_id: completion_id, metadata: metadata) do 138 | openai |> update(completion_id: completion_id, metadata: metadata) |> Http.bang_it!() 139 | end 140 | 141 | def update(openai = %OpenaiEx{}, completion_id: completion_id, metadata: metadata) do 142 | openai |> Http.post(ep_url(completion_id), json: %{metadata: metadata}) 143 | end 144 | 145 | @doc """ 146 | Deletes a stored chat completion. 147 | 148 | See https://platform.openai.com/docs/api-reference/chat/delete 149 | """ 150 | def delete!(openai = %OpenaiEx{}, completion_id: completion_id) do 151 | openai |> delete(completion_id: completion_id) |> Http.bang_it!() 152 | end 153 | 154 | def delete(openai = %OpenaiEx{}, completion_id: completion_id) do 155 | openai |> Http.delete(ep_url(completion_id)) 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 4 | "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 6 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 7 | "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, 8 | "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, 9 | "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, 10 | "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 11 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 12 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 14 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 15 | "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 16 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 17 | "multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"}, 18 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 19 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 20 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 21 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | [![License: Apache-2](https://img.shields.io/badge/License-Apache2-yellow.svg)](https://opensource.org/license/apache-2-0/) 4 | [![hex.pm badge](https://img.shields.io/hexpm/v/openai_ex.svg)](https://hex.pm/packages/openai_ex) 5 | ![hex.pm downloads](https://img.shields.io/hexpm/dw/openai_ex) 6 | 7 | `OpenaiEx` is an Elixir library that provides a battle-tested, community-maintained OpenAI API client. 8 | 9 | The main user guide is a livebook, so you should be able to run everything without any setup. The user guide is also the test suite. It is run before every version release, so it is always up to date with the library. 10 | 11 | > Portions of this project were developed with assistance from ChatGPT 3.5 and 4, as well as Claude 3 Opus and Claude Sonnets 3.5, 3.6 and 3.7. However, every line of code is human curated (by me, @restlessronin 😇). AI collaboration facilitated by [llm-context](https://github.com/cyberchitta/llm-context.py). 12 | 13 | ## Features and Benefits 14 | 15 | All API endpoints and features (as of Mar 16, 2025) are supported, including the most recent Responses API (proposed replacement for Chat Completion). The Containers and ContainerFiles APIs have also been added. 16 | 17 | ### Battle-Tested and Production-Ready 18 | 19 | This library has evolved significantly based on real-world usage and community contributions: 20 | 21 | - **Enhanced Reliability**: Includes specific improvements from production use cases such as: 22 | - API key redaction in logs to prevent credential leakage 23 | - Proper handling of SSE (Server-Sent Events) edge cases 24 | - Configurable timeouts and connection parameters 25 | - Support for non-standard SSE implementations in some LLM providers 26 | - **Broad Compatibility**: Support for Azure OpenAI, local LLMs via OpenAI-compatible proxies, and third-party integrations like Portkey 27 | 28 | ## Key Design Choices 29 | 30 | There are some important differences compared to other Elixir OpenAI wrappers: 31 | 32 | - **Python API Alignment**: Faithful mirroring of the [official Python API](https://github.com/openai/openai-python) structure. Content in memory can be uploaded directly without reading from files. 33 | 34 | - **Livebook-First Design**: Designed with Livebook compatibility in mind, relying on direct instantiation rather than application configuration. 35 | 36 | - **Finch-Based Transport**: Uses Finch for HTTP requests, which provides more control over connection pools and is well-suited for modern Elixir applications. 37 | 38 | - **Streaming Support**: Comprehensive streaming API implementations with request cancellation capabilities. 39 | 40 | - **Flexible Deployment**: 3rd Party (including local) LLMs with an OpenAI proxy, as well as the **Azure OpenAI API**, are considered legitimate use cases. 41 | 42 | Discussion and announcements are on [this thread in Elixir Forum](https://elixirforum.com/t/openai-ex-openai-api-client-library/) 43 | 44 | ## Installation and Usage 45 | 46 | For installation instructions and detailed usage examples, please look at the [User Guide on hexdocs](https://hexdocs.pm/openai_ex/userguide.html). The guide is a Livebook, and you can run all of the code in it without creating a new project. Practically every API call has a running example in the User Guide. 47 | 48 | There are also Livebook examples for: 49 | 50 | - [Streaming Orderbot](https://hexdocs.pm/openai_ex/streaming_orderbot.html) - An example of how to use ChatCompletion streaming in a Chatbot. This is a streaming version of the next Livebook in this list. 51 | - [Deeplearning.AI Orderbot](https://hexdocs.pm/openai_ex/dlai_orderbot.html) - This notebook is an elixir / Kino translation of the python notebook in [Lesson 8](https://learn.deeplearning.ai/chatgpt-prompt-eng/lesson/8/chatbot), of [Deeplearning.AI](https://www.deeplearning.ai/)'s course [ChatGPT Prompt Engineering for Developers](https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/). 52 | - [Completions Chatbot](https://hexdocs.pm/openai_ex/completions.html) - Can be deployed as a Livebook app. The deployed app displays 2 forms, one for normal completions and another for streaming completions. 53 | - [Image Generation UI](https://hexdocs.pm/openai_ex/images.html) - A simple interface for generating images with DALL-E. 54 | 55 | These are hosted on [hexdocs](https://hexdocs.pm/openai_ex) and can be used as inspiration / starters for your own projects. 56 | 57 | ## Development 58 | 59 | The following section is only for developers that want to contribute to this repository. 60 | 61 | This library was developed using a Livebook docker image that runs inside a VS Code devcontainer. The `.devcontainer` folder contains all of the relevant files. 62 | 63 | To get started, clone the repository to your local machine and open it in VS Code. Follow the prompts to open it in a container. 64 | 65 | After the container is up and running in VS Code, you can access livebook at http://localhost:8080. However, you'll need to enter a password that's stored in the environment variable `LIVEBOOK_PASSWORD`. This variable needs to be defined in the `.devcontainer/.env` file, which is explained below. 66 | 67 | ### Environment Variables and Secrets 68 | 69 | To set environment variables for devcontainer development, you can create a `.env` file in the `.devcontainer` folder. Any secrets, such as `OPENAI_API_KEY` and `LIVEBOOK_PASSWORD`, can be defined in this file as environment variables. Note that this `.env` file should not be included in version control, and it is already included in the .gitignore file for this reason. 70 | 71 | You can find a sample `env` file in the same folder, which you can use as a template for your own `.env` file. These variables will be passed to Livebook via `docker-compose.yml`. 72 | -------------------------------------------------------------------------------- /notebooks/streaming_orderbot.livemd: -------------------------------------------------------------------------------- 1 | # Streaming Orderbot 2 | 3 | ```elixir 4 | Mix.install([ 5 | {:openai_ex, "~> 0.9.18"}, 6 | {:kino, "~> 0.17.0"} 7 | ]) 8 | 9 | alias OpenaiEx 10 | alias OpenaiEx.Chat 11 | alias OpenaiEx.ChatMessage 12 | ``` 13 | 14 | ## Setup 15 | 16 | This notebook creates an Orderbot, similar to the one in [Deeplearning.AI Orderbot](https://hexdocs.pm/openai_ex/dlai_orderbot.html), but using the streaming version of the Chat Completion API. 17 | 18 | ```elixir 19 | openai = System.fetch_env!("LB_OPENAI_API_KEY") |> OpenaiEx.new() 20 | ``` 21 | 22 | ```elixir 23 | # We need to make the request task pid available to the cancel button by sharing state 24 | {:ok, pid} = Agent.start_link(fn -> nil end, name: :shared_task_pid) 25 | ``` 26 | 27 | ```elixir 28 | defmodule OpenaiEx.Notebooks.StreamingOrderbot do 29 | alias OpenaiEx 30 | require Logger 31 | 32 | def set_task_pid(task_pid) do 33 | Agent.update(:shared_task_pid, fn _ -> task_pid end) 34 | end 35 | 36 | def get_task_pid do 37 | Agent.get(:shared_task_pid, fn pid -> pid end) 38 | end 39 | 40 | def create_chat_req(args = [_ | _]) do 41 | args 42 | |> Enum.into(%{ 43 | model: "gpt-4o-mini", 44 | temperature: 0 45 | }) 46 | |> Chat.Completions.new() 47 | end 48 | 49 | def get_stream(openai = %OpenaiEx{}, messages) do 50 | openai |> Chat.Completions.create!(create_chat_req(messages: messages), stream: true) 51 | end 52 | 53 | def stream_to_completions(%{body_stream: body_stream}) do 54 | body_stream 55 | |> Stream.flat_map(& &1) 56 | |> Stream.map(fn %{data: d} -> d |> Map.get("choices") |> List.first() |> Map.get("delta") end) 57 | |> Stream.filter(fn map -> map |> Map.has_key?("content") end) 58 | |> Stream.map(fn map -> map |> Map.get("content") end) 59 | end 60 | 61 | def stream_completion_to_frame(stream, frame) do 62 | try do 63 | result = 64 | stream 65 | |> stream_to_completions() 66 | |> Enum.reduce("", fn token, text -> 67 | next = text <> token 68 | Kino.Frame.render(frame, Kino.Text.new(next)) 69 | next 70 | end) 71 | 72 | {:ok, result} 73 | rescue 74 | e in OpenaiEx.Error -> 75 | case e do 76 | %{kind: :sse_cancellation} -> 77 | message = "Request was canceled." 78 | Kino.Frame.render(frame, Kino.Text.new(message)) 79 | {:canceled, message} 80 | 81 | _ -> 82 | message = "An error occurred: #{e.message}" 83 | Kino.Frame.render(frame, Kino.Text.new(message)) 84 | {:error, message} 85 | end 86 | end 87 | end 88 | 89 | def create_orderbot(openai = %OpenaiEx{}, context) do 90 | chat_frame = Kino.Frame.new() 91 | last_frame = Kino.Frame.new() 92 | inputs = [prompt: Kino.Input.textarea("You")] 93 | form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:prompt]) 94 | cancel_button = Kino.Control.button("Cancel") 95 | Kino.Frame.render(chat_frame, Kino.Markdown.new("### Orderbot Chat")) 96 | 97 | Kino.Layout.grid([chat_frame, cancel_button, last_frame, form], boxed: true, gap: 16) 98 | |> Kino.render() 99 | 100 | stream_o = openai |> get_stream(context) 101 | {status, bot_says} = stream_o |> stream_completion_to_frame(last_frame) 102 | 103 | Kino.listen( 104 | form, 105 | context ++ if(status == :ok, do: [ChatMessage.assistant(bot_says)], else: []), 106 | fn %{data: %{prompt: you_say}}, history -> 107 | Kino.Frame.render(last_frame, Kino.Text.new("")) 108 | Kino.Frame.append(chat_frame, Kino.Text.new(List.last(history).content)) 109 | Kino.Frame.append(chat_frame, Kino.Markdown.new("**You** #{you_say}")) 110 | 111 | stream = openai |> get_stream(history ++ [ChatMessage.user(you_say)]) 112 | set_task_pid(stream.task_pid) 113 | {status, bot_says} = stream |> stream_completion_to_frame(last_frame) 114 | 115 | case status do 116 | :ok -> {:cont, history ++ [ChatMessage.user(you_say), ChatMessage.assistant(bot_says)]} 117 | _ -> {:cont, history} 118 | end 119 | end 120 | ) 121 | 122 | Kino.listen( 123 | cancel_button, 124 | fn _event -> 125 | pid = get_task_pid() 126 | OpenaiEx.HttpSse.cancel_request(pid) 127 | end 128 | ) 129 | end 130 | end 131 | 132 | alias OpenaiEx.Notebooks.StreamingOrderbot 133 | ``` 134 | 135 | ## Orderbot 136 | 137 | ```elixir 138 | context = [ 139 | ChatMessage.system(""" 140 | You are OrderBot, an automated service to collect orders for a pizza restaurant. \ 141 | You first greet the customer, then collects the order, \ 142 | and then asks if it's a pickup or delivery. \ 143 | You wait to collect the entire order, then summarize it and check for a final \ 144 | time if the customer wants to add anything else. \ 145 | If it's a delivery, you ask for an address. \ 146 | Finally you collect the payment.\ 147 | Make sure to clarify all options, extras and sizes to uniquely \ 148 | identify the item from the menu.\ 149 | You respond in a short, very conversational friendly style. \ 150 | The menu includes \ 151 | pepperoni pizza 12.95, 10.00, 7.00 \ 152 | cheese pizza 10.95, 9.25, 6.50 \ 153 | eggplant pizza 11.95, 9.75, 6.75 \ 154 | fries 4.50, 3.50 \ 155 | greek salad 7.25 \ 156 | Toppings: \ 157 | extra cheese 2.00, \ 158 | mushrooms 1.50 \ 159 | sausage 3.00 \ 160 | canadian bacon 3.50 \ 161 | AI sauce 1.50 \ 162 | peppers 1.00 \ 163 | Drinks: \ 164 | coke 3.00, 2.00, 1.00 \ 165 | sprite 3.00, 2.00, 1.00 \ 166 | bottled water 5.00 \ 167 | """) 168 | ] 169 | ``` 170 | 171 | ```elixir 172 | openai |> StreamingOrderbot.create_orderbot(context) 173 | ``` 174 | -------------------------------------------------------------------------------- /notebooks/dlai_orderbot.livemd: -------------------------------------------------------------------------------- 1 | # Deeplearning.AI Order Bot 2 | 3 | ```elixir 4 | Mix.install([ 5 | {:openai_ex, "~> 0.9.18"}, 6 | {:kino, "~> 0.17.0"} 7 | ]) 8 | 9 | alias OpenaiEx 10 | alias OpenaiEx.Chat 11 | alias OpenaiEx.ChatMessage 12 | ``` 13 | 14 | ## Source 15 | 16 | This notebook is an elixir translation of the python notebook in [Lesson 8](https://learn.deeplearning.ai/chatgpt-prompt-eng/lesson/8/chatbot), of [Deeplearning.AI](https://www.deeplearning.ai/)'s course [ChatGPT Prompt Engineering for Developers](https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/). 17 | 18 | ## Chat Format 19 | 20 | In this notebook, you will explore how you can utilize the chat format to have extended conversations with chatbots personalized or specialized for specific tasks or behaviors. 21 | 22 | ### Setup 23 | 24 | ```elixir 25 | openai = 26 | System.fetch_env!("LB_OPENAI_API_KEY") 27 | |> OpenaiEx.new() 28 | 29 | # uncomment the line at the end of this block comment when working with a local LLM with a 30 | # proxy such as llama.cpp-python in the example below, our development livebook server is 31 | # running in a docker dev container while the local llm is running on the host machine 32 | # |> OpenaiEx.with_base_url("http://host.docker.internal:8000/v1") 33 | ``` 34 | 35 | ```elixir 36 | defmodule OpenaiEx.Notebooks.DlaiOrderbot do 37 | alias OpenaiEx 38 | alias OpenaiEx.Chat 39 | 40 | def create_chat_req(args = [_ | _]) do 41 | args 42 | |> Enum.into(%{ 43 | model: "gpt-4o-mini", 44 | temperature: 0 45 | }) 46 | |> Chat.Completions.new() 47 | end 48 | 49 | def get_completion(openai = %OpenaiEx{}, cc_req = %{}) do 50 | openai 51 | |> Chat.Completions.create!(cc_req) 52 | # for debugging 53 | # |> IO.inspect() 54 | |> Map.get("choices") 55 | |> List.first() 56 | |> Map.get("message") 57 | |> Map.get("content") 58 | end 59 | end 60 | 61 | alias OpenaiEx.Notebooks.DlaiOrderbot 62 | ``` 63 | 64 | ```elixir 65 | messages = [ 66 | ChatMessage.system("You are an assistant that speaks like Shakespeare."), 67 | ChatMessage.user("tell me a joke"), 68 | ChatMessage.assistant("Why did the chicken cross the road"), 69 | ChatMessage.user("I don't know") 70 | ] 71 | 72 | req = DlaiOrderbot.create_chat_req(messages: messages, temperature: 1) 73 | openai |> DlaiOrderbot.get_completion(req) 74 | ``` 75 | 76 | ```elixir 77 | messages = [ 78 | ChatMessage.system("You are friendly chatbot."), 79 | ChatMessage.user("Hi, my name is Isa") 80 | ] 81 | 82 | req = DlaiOrderbot.create_chat_req(messages: messages, temperature: 1) 83 | openai |> DlaiOrderbot.get_completion(req) 84 | ``` 85 | 86 | ```elixir 87 | messages = [ 88 | ChatMessage.system("You are friendly chatbot."), 89 | ChatMessage.user("Yes, can you remind me, What is my name?") 90 | ] 91 | 92 | req = DlaiOrderbot.create_chat_req(messages: messages, temperature: 1) 93 | openai |> DlaiOrderbot.get_completion(req) 94 | ``` 95 | 96 | ```elixir 97 | messages = [ 98 | ChatMessage.system("You are friendly chatbot."), 99 | ChatMessage.user("Hi, my name is Isa"), 100 | ChatMessage.assistant( 101 | "Hi Isa! It's nice to meet you. Is there anything I can help you with today?" 102 | ), 103 | ChatMessage.user("Yes, can you remind me, What is my name?") 104 | ] 105 | 106 | req = DlaiOrderbot.create_chat_req(messages: messages, temperature: 1) 107 | openai |> DlaiOrderbot.get_completion(req) 108 | ``` 109 | 110 | ## Order Bot 111 | 112 | We can automate the collection of user prompts and assistant responses to build a OrderBot. The OrderBot will take orders at a pizza restaurant. 113 | 114 | ```elixir 115 | context = [ 116 | ChatMessage.system(""" 117 | You are OrderBot, an automated service to collect orders for a pizza restaurant. \ 118 | You first greet the customer, then collect the order, \ 119 | and then ask if it's a pickup or delivery. \ 120 | You wait to collect the entire order, then summarize it and check for a final \ 121 | time if the customer wants to add anything else. \ 122 | If it's a delivery, you ask for an address. \ 123 | Finally you collect the payment.\ 124 | Make sure to clarify all options, extras and sizes to uniquely \ 125 | identify the item from the menu.\ 126 | You respond in a short, very conversational friendly style. \ 127 | The menu includes \ 128 | pepperoni pizza 12.95, 10.00, 7.00 \ 129 | cheese pizza 10.95, 9.25, 6.50 \ 130 | eggplant pizza 11.95, 9.75, 6.75 \ 131 | fries 4.50, 3.50 \ 132 | greek salad 7.25 \ 133 | Toppings: \ 134 | extra cheese 2.00, \ 135 | mushrooms 1.50 \ 136 | sausage 3.00 \ 137 | canadian bacon 3.50 \ 138 | AI sauce 1.50 \ 139 | peppers 1.00 \ 140 | Drinks: \ 141 | coke 3.00, 2.00, 1.00 \ 142 | sprite 3.00, 2.00, 1.00 \ 143 | bottled water 5.00 \ 144 | """) 145 | ] 146 | ``` 147 | 148 | ```elixir 149 | append_completion = fn openai, messages, frame -> 150 | req = DlaiOrderbot.create_chat_req(messages: messages) 151 | bot_says = openai |> DlaiOrderbot.get_completion(req) 152 | Kino.Frame.append(frame, Kino.Markdown.new("#{bot_says}")) 153 | bot_says 154 | end 155 | 156 | chat_frame = Kino.Frame.new() 157 | inputs = [prompt: Kino.Input.textarea("You")] 158 | form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:prompt]) 159 | Kino.Frame.render(chat_frame, Kino.Markdown.new("### Orderbot Chat")) 160 | Kino.Layout.grid([chat_frame, form], boxed: true, gap: 16) |> Kino.render() 161 | 162 | bot_says = openai |> append_completion.(context, chat_frame) 163 | 164 | Kino.listen( 165 | form, 166 | context ++ [ChatMessage.assistant(bot_says)], 167 | fn %{data: %{prompt: you_say}}, history -> 168 | Kino.Frame.append(chat_frame, Kino.Markdown.new("**You** #{you_say}")) 169 | 170 | bot_says = openai |> append_completion.(history ++ [ChatMessage.user(you_say)], chat_frame) 171 | 172 | {:cont, history ++ [ChatMessage.user(you_say), ChatMessage.assistant(bot_says)]} 173 | end 174 | ) 175 | ``` 176 | -------------------------------------------------------------------------------- /lib/openai_ex/http_sse.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.HttpSse do 2 | @moduledoc false 3 | alias OpenaiEx.HttpFinch 4 | alias OpenaiEx.Error 5 | require Logger 6 | 7 | # based on 8 | # https://gist.github.com/zachallaun/88aed2a0cef0aed6d68dcc7c12531649 9 | # and 10 | # https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream 11 | 12 | def post(openai = %OpenaiEx{}, url, json: json) do 13 | me = self() 14 | ref = make_ref() 15 | request = HttpFinch.build_post(openai, url, json: json) 16 | task = Task.async(fn -> finch_stream(openai, request, me, ref) end) 17 | result = build_sse_stream(openai, task, request, ref) 18 | unless match?({:ok, %{task_pid: _}}, result), do: Task.shutdown(task) 19 | result 20 | end 21 | 22 | def cancel_request(task_pid) when is_pid(task_pid) do 23 | send(task_pid, :cancel_request) 24 | end 25 | 26 | defp build_sse_stream(openai, task, request, ref) do 27 | with {:ok, status} <- receive_with_timeout(ref, :status, openai.receive_timeout), 28 | {:ok, headers} <- receive_with_timeout(ref, :headers, openai.receive_timeout) do 29 | if status in 200..299 do 30 | stream_receiver = create_stream_receiver(ref, openai.stream_timeout) 31 | body_stream = Stream.resource(&init_stream/0, stream_receiver, end_stream(task)) 32 | {:ok, %{status: status, headers: headers, body_stream: body_stream, task_pid: task.pid}} 33 | else 34 | with {:ok, body} <- extract_error(ref, "", openai.receive_timeout) do 35 | response = %{status: status, headers: headers, body: body} 36 | {:error, Error.status_error(status, response, body)} 37 | else 38 | error_result -> handle_receive_error(error_result, request) 39 | end 40 | end 41 | else 42 | error_result -> handle_receive_error(error_result, request) 43 | end 44 | end 45 | 46 | defp handle_receive_error(error_result, request) do 47 | case error_result do 48 | :error -> {:error, Error.api_timeout_error(request)} 49 | {:error, {:stream_error, exception}} -> HttpFinch.to_error(exception, request) 50 | end 51 | end 52 | 53 | defp finch_stream(openai = %OpenaiEx{}, request, me, ref) do 54 | try do 55 | case HttpFinch.stream(request, openai, create_chunk_sender(me, ref)) do 56 | {:ok, _acc} -> send(me, {:done, ref}) 57 | {:error, exception, _acc} -> send(me, {:stream_error, exception, ref}) 58 | end 59 | catch 60 | :throw, :cancel_request -> {:exception, :cancel_request} 61 | end 62 | end 63 | 64 | defp create_chunk_sender(me, ref) do 65 | fn chunk, _acc -> 66 | receive do 67 | :cancel_request -> 68 | send(me, {:canceled, ref}) 69 | throw(:cancel_request) 70 | after 71 | 0 -> send(me, {:chunk, chunk, ref}) 72 | end 73 | end 74 | end 75 | 76 | defp receive_with_timeout(ref, type, timeout) do 77 | receive do 78 | {:chunk, {^type, value}, ^ref} -> {:ok, value} 79 | {:stream_error, exception, ^ref} -> {:error, {:stream_error, exception}} 80 | after 81 | timeout -> :error 82 | end 83 | end 84 | 85 | defp init_stream, do: "" 86 | 87 | defp create_stream_receiver(ref, timeout) do 88 | fn acc when is_binary(acc) -> 89 | receive do 90 | {:chunk, {:data, evt_data}, ^ref} -> 91 | {events, next_acc} = extract_events(evt_data, acc) 92 | {[events], next_acc} 93 | 94 | # some 3rd party providers seem to be ending the stream with eof, 95 | # rather than 2 line terminators. Hopefully those will be fixed and this 96 | # can be removed in the future 97 | {:done, ^ref} when acc == "data: [DONE]" -> 98 | Logger.warning("\"data: [DONE]\" should be followed by 2 line terminators") 99 | {:halt, acc} 100 | 101 | {:done, ^ref} -> 102 | {:halt, acc} 103 | 104 | {:stream_error, exception, ^ref} -> 105 | Logger.warning("Finch stream error: #{inspect(exception)}") 106 | {:halt, {:exception, {:stream_error, exception}}} 107 | 108 | {:canceled, ^ref} -> 109 | Logger.info("Request canceled by user") 110 | {:halt, {:exception, :canceled}} 111 | after 112 | timeout -> 113 | Logger.warning("Stream timeout after #{timeout}ms") 114 | {:halt, {:exception, :timeout}} 115 | end 116 | end 117 | end 118 | 119 | defp end_stream(task) do 120 | fn acc -> 121 | try do: 122 | (case acc do 123 | {:exception, :timeout} -> raise(Error.sse_timeout_error()) 124 | {:exception, :canceled} -> raise(Error.sse_user_cancellation()) 125 | {:exception, {:stream_error, exception}} -> raise(HttpFinch.to_error!(exception, nil)) 126 | _ -> :ok 127 | end), 128 | after: Task.shutdown(task) 129 | end 130 | end 131 | 132 | @double_eol ~r/(\r?\n|\r){2}/ 133 | @double_eol_eos ~r/(\r?\n|\r){2}$/ 134 | 135 | defp extract_events(evt_data, acc) do 136 | all_data = acc <> evt_data 137 | 138 | if Regex.match?(@double_eol, all_data) do 139 | {remaining, lines} = extract_lines(all_data) 140 | events = process_fields(lines) 141 | {events, remaining} 142 | else 143 | {[], all_data} 144 | end 145 | end 146 | 147 | defp extract_lines(data) do 148 | lines = String.split(data, @double_eol) 149 | incomplete_line = !Regex.match?(@double_eol_eos, data) 150 | if incomplete_line, do: lines |> List.pop_at(-1), else: {"", lines} 151 | end 152 | 153 | defp process_fields(lines) do 154 | lines 155 | |> Enum.map(&extract_field/1) 156 | |> Enum.filter(&data?/1) 157 | |> Enum.map(&to_json/1) 158 | end 159 | 160 | defp to_json(field) do 161 | case field do 162 | %{data: data} -> 163 | %{data: Jason.decode!(data)} 164 | 165 | %{eventType: value} -> 166 | [event_id, data] = String.split(value, "\ndata: ", parts: 2) 167 | %{event: event_id, data: Jason.decode!(data)} 168 | end 169 | end 170 | 171 | defp data?(field) do 172 | case field do 173 | %{data: "[DONE]"} -> false 174 | %{data: _} -> true 175 | %{eventType: "done\ndata: [DONE]"} -> false 176 | %{eventType: _} -> true 177 | _ -> false 178 | end 179 | end 180 | 181 | defp extract_field(line) do 182 | [name | rest] = String.split(line, ":", parts: 2) 183 | value = Enum.join(rest, "") |> String.replace_prefix(" ", "") 184 | 185 | case name do 186 | "data" -> %{data: value} 187 | "event" -> %{eventType: value} 188 | "id" -> %{lastEventId: value} 189 | "retry" -> %{retry: value} 190 | _ -> nil 191 | end 192 | end 193 | 194 | defp extract_error(ref, acc, timeout) do 195 | receive do 196 | {:chunk, {:data, chunk}, ^ref} -> extract_error(ref, acc <> chunk, timeout) 197 | {:done, ^ref} -> {:ok, Jason.decode!(acc)} 198 | {:stream_error, exception, ^ref} -> {:error, {:stream_error, exception}} 199 | after 200 | timeout -> :error 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /lib/openai_ex/beta/threads_runs.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Beta.Threads.Runs do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI run API. The API 4 | reference can be found at https://platform.openai.com/docs/api-reference/runs. 5 | """ 6 | alias OpenaiEx.{Http, HttpSse} 7 | 8 | @api_fields [ 9 | :assistant_id, 10 | :model, 11 | :instructions, 12 | :additional_instructions, 13 | :additional_messages, 14 | :tools, 15 | :metadata, 16 | :temperature, 17 | :top_p, 18 | :max_prompt_tokens, 19 | :max_completion_tokens, 20 | :truncation_strategy, 21 | :tool_choice, 22 | :response_format 23 | ] 24 | @query_params [ 25 | :include 26 | ] 27 | 28 | @ep_url "/threads/runs" 29 | 30 | defp ep_url(thread_id, run_id \\ nil, action \\ nil) do 31 | "/threads/#{thread_id}/runs" <> 32 | if(is_nil(run_id), do: "", else: "/#{run_id}") <> 33 | if(is_nil(action), do: "", else: "/#{action}") 34 | end 35 | 36 | @doc """ 37 | Creates a new run request 38 | 39 | Example usage: 40 | 41 | iex> _request = OpenaiEx.Beta.Threads.Runs.new(thread_id: "thread_foo", assistant_id: "assistant_bar") 42 | %{assistant_id: "assistant_bar", thread_id: "thread_foo"} 43 | """ 44 | 45 | def new(args = [_ | _]) do 46 | args |> Enum.into(%{}) |> new() 47 | end 48 | 49 | def new(args = %{thread_id: _}) do 50 | args |> Map.take([:thread_id] ++ @api_fields ++ @query_params) 51 | end 52 | 53 | @doc """ 54 | Calls the run create endpoint. 55 | 56 | See https://platform.openai.com/docs/api-reference/runs/createRun for more information. 57 | """ 58 | def create!(openai = %OpenaiEx{}, run = %{thread_id: _, assistant_id: _}, stream: true) do 59 | openai |> create(run, stream: true) |> Http.bang_it!() 60 | end 61 | 62 | def create(openai = %OpenaiEx{}, run = %{thread_id: thread_id, assistant_id: _}, stream: true) do 63 | json = run |> Map.take(@api_fields) |> Map.put(:stream, true) 64 | url = Http.build_url(ep_url(thread_id), run |> Map.take(@query_params)) 65 | openai |> OpenaiEx.with_assistants_beta() |> HttpSse.post(url, json: json) 66 | end 67 | 68 | def create!(openai = %OpenaiEx{}, run = %{thread_id: _, assistant_id: _}) do 69 | openai |> create(run) |> Http.bang_it!() 70 | end 71 | 72 | def create(openai = %OpenaiEx{}, run = %{thread_id: thread_id, assistant_id: _}) do 73 | json = run |> Map.take(@api_fields) 74 | url = Http.build_url(ep_url(thread_id), run |> Map.take(@query_params)) 75 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(url, json: json) 76 | end 77 | 78 | @doc """ 79 | Calls the run retrieve endpoint. 80 | 81 | https://platform.openai.com/docs/api-reference/runs/getRun 82 | """ 83 | def retrieve!(openai = %OpenaiEx{}, params = %{thread_id: _, run_id: _}) do 84 | openai |> retrieve(params) |> Http.bang_it!() 85 | end 86 | 87 | def retrieve(openai = %OpenaiEx{}, %{thread_id: thread_id, run_id: run_id}) do 88 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(thread_id, run_id)) 89 | end 90 | 91 | @doc """ 92 | Calls the run update endpoint. 93 | 94 | See https://platform.openai.com/docs/api-reference/assistants/modifyAssistant for more information. 95 | """ 96 | def update!(openai = %OpenaiEx{}, params = %{thread_id: _, run_id: _, metadata: _}) do 97 | openai |> update(params) |> Http.bang_it!() 98 | end 99 | 100 | def update(openai = %OpenaiEx{}, %{thread_id: thread_id, run_id: run_id, metadata: metadata}) do 101 | json = %{metadata: metadata} 102 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(thread_id, run_id), json: json) 103 | end 104 | 105 | @doc """ 106 | Creates a new list runs request 107 | """ 108 | 109 | def new_list(args = [_ | _]) do 110 | args |> Enum.into(%{}) |> new_list() 111 | end 112 | 113 | def new_list(args = %{}) do 114 | args 115 | |> Map.take(OpenaiEx.list_query_fields() ++ @query_params) 116 | end 117 | 118 | @doc """ 119 | Returns a list of runs objects. 120 | 121 | https://platform.openai.com/docs/api-reference/runs/listRuns 122 | """ 123 | def list!(openai = %OpenaiEx{}, opts) when is_list(opts) do 124 | openai |> list(opts) |> Http.bang_it!() 125 | end 126 | 127 | def list(openai = %OpenaiEx{}, opts) when is_list(opts) do 128 | thread_id = Keyword.fetch!(opts, :thread_id) 129 | 130 | params = 131 | opts 132 | |> Keyword.drop([:thread_id]) 133 | |> Enum.into(%{}) 134 | |> Map.take(OpenaiEx.list_query_fields() ++ @query_params) 135 | 136 | openai |> OpenaiEx.with_assistants_beta() |> Http.get(ep_url(thread_id), params) 137 | end 138 | 139 | def submit_tool_outputs!( 140 | openai = %OpenaiEx{}, 141 | params = %{thread_id: _, run_id: _, tool_outputs: _}, 142 | stream: true 143 | ) do 144 | openai |> submit_tool_outputs(params, stream: true) |> Http.bang_it!() 145 | end 146 | 147 | def submit_tool_outputs( 148 | openai = %OpenaiEx{}, 149 | %{thread_id: thread_id, run_id: run_id, tool_outputs: tool_outputs}, 150 | stream: true 151 | ) do 152 | openai 153 | |> OpenaiEx.with_assistants_beta() 154 | |> HttpSse.post( 155 | ep_url(thread_id, run_id, "submit_tool_outputs"), 156 | json: %{tool_outputs: tool_outputs} |> Map.put(:stream, true) 157 | ) 158 | end 159 | 160 | def submit_tool_outputs!( 161 | openai = %OpenaiEx{}, 162 | params = %{thread_id: _, run_id: _, tool_outputs: _} 163 | ) do 164 | openai |> submit_tool_outputs(params) |> Http.bang_it!() 165 | end 166 | 167 | def submit_tool_outputs( 168 | openai = %OpenaiEx{}, 169 | %{thread_id: thread_id, run_id: run_id, tool_outputs: tool_outputs} 170 | ) do 171 | openai 172 | |> OpenaiEx.with_assistants_beta() 173 | |> Http.post( 174 | ep_url(thread_id, run_id, "submit_tool_outputs"), 175 | json: %{tool_outputs: tool_outputs} 176 | ) 177 | end 178 | 179 | def cancel!(openai = %OpenaiEx{}, params = %{thread_id: _, run_id: _}) do 180 | openai |> cancel(params) |> Http.bang_it!() 181 | end 182 | 183 | def cancel(openai = %OpenaiEx{}, %{thread_id: thread_id, run_id: run_id}) do 184 | openai |> OpenaiEx.with_assistants_beta() |> Http.post(ep_url(thread_id, run_id, "cancel")) 185 | end 186 | 187 | @car_fields [ 188 | :assistant_id, 189 | :thread, 190 | :model, 191 | :instructions, 192 | :tools, 193 | :tool_resources, 194 | :metadata, 195 | :temperature, 196 | :top_p, 197 | :max_prompt_tokens, 198 | :max_completion_tokens, 199 | :truncation_strategy, 200 | :tool_choice, 201 | :response_format 202 | ] 203 | 204 | def create_and_run!(openai = %OpenaiEx{}, params = %{assistant_id: _}, stream: true) do 205 | openai |> create_and_run(params, stream: true) |> Http.bang_it!() 206 | end 207 | 208 | def create_and_run(openai = %OpenaiEx{}, params = %{assistant_id: _}, stream: true) do 209 | openai 210 | |> OpenaiEx.with_assistants_beta() 211 | |> HttpSse.post(@ep_url, 212 | json: params |> Map.take(@car_fields) |> Map.put(:stream, true) 213 | ) 214 | end 215 | 216 | def create_and_run!(openai = %OpenaiEx{}, params = %{assistant_id: _}) do 217 | openai |> create_and_run(params) |> Http.bang_it!() 218 | end 219 | 220 | def create_and_run(openai = %OpenaiEx{}, params = %{assistant_id: _}) do 221 | openai 222 | |> OpenaiEx.with_assistants_beta() 223 | |> Http.post(@ep_url, json: params |> Map.take(@car_fields)) 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /lib/openai_ex/evals.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenaiEx.Evals do 2 | @moduledoc """ 3 | This module provides an implementation of the OpenAI evals API. The API reference can be found at https://platform.openai.com/docs/api-reference/evals. 4 | """ 5 | alias OpenaiEx.Http 6 | 7 | @api_fields [ 8 | :data_source_config, 9 | :testing_criteria, 10 | :metadata, 11 | :name 12 | ] 13 | 14 | @run_api_fields [ 15 | :data_source, 16 | :metadata, 17 | :name 18 | ] 19 | 20 | defp ep_url(eval_id \\ nil, action \\ nil, run_id \\ nil, output_item_id \\ nil) do 21 | base = 22 | "/evals" <> 23 | if(is_nil(eval_id), do: "", else: "/#{eval_id}") <> 24 | if(is_nil(action), do: "", else: "/#{action}") 25 | 26 | case {run_id, output_item_id} do 27 | {nil, nil} -> base 28 | {run_id, nil} -> base <> "/#{run_id}" 29 | {run_id, output_item_id} -> base <> "/#{run_id}/output_items/#{output_item_id}" 30 | end 31 | end 32 | 33 | @doc """ 34 | Creates a new eval request 35 | 36 | See https://platform.openai.com/docs/api-reference/evals/create for full list of request parameters. 37 | 38 | Example usage: 39 | 40 | iex> _request = OpenaiEx.Evals.new(name: "Sentiment") 41 | %{name: "Sentiment"} 42 | 43 | iex> _request = OpenaiEx.Evals.new(%{name: "Sentiment", testing_criteria: [%{type: "label_model", ...}]}) 44 | %{name: "Sentiment", testing_criteria: [%{type: "label_model", ...}]} 45 | """ 46 | def new(args = [_ | _]) do 47 | args |> Enum.into(%{}) |> new() 48 | end 49 | 50 | def new(args = %{name: name}) do 51 | %{ 52 | name: name 53 | } 54 | |> Map.merge(args) 55 | |> Map.take(@api_fields) 56 | end 57 | 58 | @doc """ 59 | Creates a new eval run request 60 | 61 | See https://platform.openai.com/docs/api-reference/evals/createRun for full list of request parameters. 62 | 63 | Example usage: 64 | 65 | iex> _request = OpenaiEx.Evals.new_run(name: "gpt-4o-mini") 66 | %{name: "gpt-4o-mini"} 67 | 68 | iex> _request = OpenaiEx.Evals.new_run(%{name: "gpt-4o-mini", data_source: %{type: "completions", ...}}) 69 | %{name: "gpt-4o-mini", data_source: %{type: "completions", ...}} 70 | """ 71 | def new_run(args = [_ | _]) do 72 | args |> Enum.into(%{}) |> new_run() 73 | end 74 | 75 | def new_run(args = %{name: name}) do 76 | %{ 77 | name: name 78 | } 79 | |> Map.merge(args) 80 | |> Map.take(@run_api_fields) 81 | end 82 | 83 | @doc """ 84 | Calls the eval 'create' endpoint. 85 | 86 | See https://platform.openai.com/docs/api-reference/evals/create for more information. 87 | """ 88 | def create!(openai = %OpenaiEx{}, eval_request = %{}) do 89 | openai |> create(eval_request) |> Http.bang_it!() 90 | end 91 | 92 | def create(openai = %OpenaiEx{}, eval_request = %{}) do 93 | json = eval_request |> Map.take(@api_fields) 94 | openai |> Http.post(ep_url(), json: json) 95 | end 96 | 97 | @doc """ 98 | Calls the eval update endpoint. 99 | 100 | See https://platform.openai.com/docs/api-reference/evals/update for more information. 101 | """ 102 | def update!(openai = %OpenaiEx{}, eval_id, eval_request = %{}) do 103 | openai |> update(eval_id, eval_request) |> Http.bang_it!() 104 | end 105 | 106 | def update(openai = %OpenaiEx{}, eval_id, eval_request = %{}) do 107 | json = eval_request |> Map.take(@api_fields) 108 | openai |> Http.post(ep_url(eval_id), json: json) 109 | end 110 | 111 | @doc """ 112 | Calls the eval delete endpoint. 113 | 114 | See https://platform.openai.com/docs/api-reference/evals/delete for more information. 115 | """ 116 | def delete!(openai = %OpenaiEx{}, eval_id) do 117 | openai |> delete(eval_id) |> Http.bang_it!() 118 | end 119 | 120 | def delete(openai = %OpenaiEx{}, eval_id) do 121 | openai |> Http.delete(ep_url(eval_id)) 122 | end 123 | 124 | @doc """ 125 | Lists all evals that belong to the user's organization. 126 | 127 | See https://platform.openai.com/docs/api-reference/evals/list for more information. 128 | """ 129 | def list!(openai = %OpenaiEx{}, params \\ %{}) do 130 | openai |> list(params) |> Http.bang_it!() 131 | end 132 | 133 | def list(openai = %OpenaiEx{}, params \\ %{}) do 134 | query_params = params |> Map.take(OpenaiEx.list_query_fields()) 135 | openai |> Http.get(ep_url(), query_params) 136 | end 137 | 138 | # Run-related functions 139 | 140 | @doc """ 141 | Lists all runs for a specific evaluation. 142 | 143 | See https://platform.openai.com/docs/api-reference/evals/getRuns for more information. 144 | """ 145 | def get_runs!(openai = %OpenaiEx{}, eval_id) do 146 | openai |> get_runs(eval_id) |> Http.bang_it!() 147 | end 148 | 149 | def get_runs(openai = %OpenaiEx{}, eval_id) do 150 | openai |> Http.get(ep_url(eval_id, "runs")) 151 | end 152 | 153 | @doc """ 154 | Retrieves a specific run for an evaluation. 155 | 156 | See https://platform.openai.com/docs/api-reference/evals/getRun for more information. 157 | """ 158 | def get_run!(openai = %OpenaiEx{}, eval_id, run_id) do 159 | openai |> get_run(eval_id, run_id) |> Http.bang_it!() 160 | end 161 | 162 | def get_run(openai = %OpenaiEx{}, eval_id, run_id) do 163 | openai |> Http.get(ep_url(eval_id, "runs", run_id)) 164 | end 165 | 166 | @doc """ 167 | Creates a new run for an evaluation. 168 | 169 | See https://platform.openai.com/docs/api-reference/evals/createRun for more information. 170 | """ 171 | def create_run!(openai = %OpenaiEx{}, eval_id, run_request = %{}) do 172 | openai |> create_run(eval_id, run_request) |> Http.bang_it!() 173 | end 174 | 175 | def create_run(openai = %OpenaiEx{}, eval_id, run_request = %{}) do 176 | json = run_request |> Map.take(@run_api_fields) 177 | openai |> Http.post(ep_url(eval_id, "runs"), json: json) 178 | end 179 | 180 | @doc """ 181 | Cancels a specific run for an evaluation. 182 | 183 | See https://platform.openai.com/docs/api-reference/evals/cancelRun for more information. 184 | """ 185 | def cancel_run!(openai = %OpenaiEx{}, eval_id, run_id) do 186 | openai |> cancel_run(eval_id, run_id) |> Http.bang_it!() 187 | end 188 | 189 | def cancel_run(openai = %OpenaiEx{}, eval_id, run_id) do 190 | openai |> Http.post(ep_url(eval_id, "runs", run_id, "cancel"), json: %{}) 191 | end 192 | 193 | @doc """ 194 | Deletes a specific run for an evaluation. 195 | 196 | See https://platform.openai.com/docs/api-reference/evals/deleteRun for more information. 197 | """ 198 | def delete_run!(openai = %OpenaiEx{}, eval_id, run_id) do 199 | openai |> delete_run(eval_id, run_id) |> Http.bang_it!() 200 | end 201 | 202 | def delete_run(openai = %OpenaiEx{}, eval_id, run_id) do 203 | openai |> Http.delete(ep_url(eval_id, "runs", run_id)) 204 | end 205 | 206 | # Output item-related functions 207 | 208 | @doc """ 209 | Retrieves a specific output item for a run. 210 | 211 | See https://platform.openai.com/docs/api-reference/evals/getRunOutputItem for more information. 212 | """ 213 | def get_run_output_item!(openai = %OpenaiEx{}, eval_id, run_id, output_item_id) do 214 | openai |> get_run_output_item(eval_id, run_id, output_item_id) |> Http.bang_it!() 215 | end 216 | 217 | def get_run_output_item(openai = %OpenaiEx{}, eval_id, run_id, output_item_id) do 218 | openai |> Http.get(ep_url(eval_id, "runs", run_id, output_item_id)) 219 | end 220 | 221 | @doc """ 222 | Lists all output items for a specific run. 223 | 224 | See https://platform.openai.com/docs/api-reference/evals/listRunOutputItems for more information. 225 | """ 226 | def list_run_output_items!(openai = %OpenaiEx{}, eval_id, run_id) do 227 | openai |> list_run_output_items(eval_id, run_id) |> Http.bang_it!() 228 | end 229 | 230 | def list_run_output_items(openai = %OpenaiEx{}, eval_id, run_id) do 231 | openai |> Http.get(ep_url(eval_id, "runs", run_id) <> "/output_items") 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /.llm-context/rules/lc/ins-rule-framework.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Provides a decision framework, semantics, and best practices for creating task-focused rules, including file selection patterns and composition guidelines. Use as core guidance for building custom rules for context generation. 3 | --- 4 | 5 | ## Decision Framework 6 | 7 | Create task-focused rules by selecting the minimal set of files needed for your objective, using the following rule categories: 8 | 9 | - **Prompt Rules (`prm-`)**: Generate project contexts (e.g., `lc/prm-developer` for code files, `lc/prm-rule-create` for rule creation tasks). 10 | - **Filter Rules (`flt-`)**: Control file inclusion/exclusion (e.g., `lc/flt-base` for standard exclusions, `lc/flt-no-files` for minimal contexts). 11 | - **Excerpting Rules (`exc-`)**: Configure code outlining and structure extraction (e.g., `lc/exc-base` for standard code outlining). 12 | - **Instruction Rules (`ins-`)**: Provide guidance (e.g., `lc/ins-developer` for developer guidelines, `lc/ins-rule-intro` for chat-based rule creation). 13 | - **Style Rules (`sty-`)**: Enforce coding standards (e.g., `lc/sty-python` for Python-specific style, `lc/sty-code` for universal principles). 14 | 15 | ### Quick Decision Guide 16 | 17 | - **Need detailed code implementations?** → Use `lc/prm-developer` for full content or specific `also-include` patterns. 18 | - **Need only code structure/outlines?** → Use `lc/flt-no-full` with `also-include` for excerpted files. 19 | - **Need coding style guidelines?** → Include `lc/sty-code`, `lc/sty-python`, etc., for relevant languages. 20 | - **Need minimal context (metadata/notes)?** → Use `lc/flt-no-files`. 21 | - **Need precise file control over a small set?** → Use `lc/flt-no-files` with explicit `also-include` patterns. 22 | - **Need rule creation guidance?** → Compose with `lc/ins-rule-intro` or this rule (`lc/ins-rule-framework`). 23 | 24 | ## Code Outlining & Excerpting System 25 | 26 | The excerpting system provides structured views of your code through different modes: 27 | 28 | ### Code Outlining (Primary Mode) 29 | 30 | **Files Supported**: `.c`, `.cc`, `.cpp`, `.cs`, `.el`, `.ex`, `.elm`, `.go`, `.java`, `.js`, `.mjs`, `.php`, `.py`, `.rb`, `.rs`, `.ts` 31 | 32 | Code outlining extracts function/class definitions and key structural elements, showing: 33 | 34 | - Function signatures with `█` markers for definitions 35 | - Class declarations and methods 36 | - Important structural code with `│` continuation markers 37 | - Condensed view with `⋮...` for omitted sections 38 | 39 | ### SFC Excerpting (Single File Components) 40 | 41 | **Files Supported**: `.svelte`, `.vue` 42 | 43 | Extracts script sections from Single File Components while preserving structure: 44 | 45 | - Always includes `