Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- `Langfuse.Prompt.get/2` now returns prompt data correctly; the underlying HTTP call was sending the prompt name as a query parameter instead of in the URL path

### Changed
- Relaxed Elixir version constraint from `~> 1.19` to `~> 1.17` to support projects on Elixir 1.17 and 1.18

### Added
- `:resolve` option for `Langfuse.Prompt.get/2`, `Langfuse.Prompt.fetch/2`, `Langfuse.Client.get_prompt/2`, and `Langfuse.HTTP.get_prompt/2` to control server-side prompt dependency resolution
- GitHub Actions CI with matrix testing across Elixir 1.17/OTP 26, 1.18/OTP 27, and 1.19/OTP 28

## [0.1.0] - 2025-11-29
Expand Down
1 change: 1 addition & 0 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

packages = [
pkgs.git
pkgs.nodejs
];

enterShell = ''
Expand Down
14 changes: 10 additions & 4 deletions lib/langfuse/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ defmodule Langfuse.Client do
@spec update_prompt_labels(String.t(), pos_integer(), keyword()) :: response()
def update_prompt_labels(name, version, opts) do
body = %{labels: Keyword.fetch!(opts, :labels)}
patch("/api/public/v2/prompts/#{URI.encode(name)}/versions/#{version}", body)

patch(
"/api/public/v2/prompts/#{URI.encode(name, &URI.char_unreserved?/1)}/versions/#{version}",
body
)
end

@doc """
Expand All @@ -167,6 +171,7 @@ defmodule Langfuse.Client do

* `:version` - Specific version number to fetch
* `:label` - Label to fetch (e.g., "production", "latest")
* `:resolve` - Whether to resolve prompt dependencies before returning (defaults to `true` on server)

## Examples

Expand All @@ -184,8 +189,9 @@ defmodule Langfuse.Client do
[]
|> maybe_add_param(:version, opts[:version])
|> maybe_add_param(:label, opts[:label])
|> maybe_add_param(:resolve, opts[:resolve])

get("/api/public/v2/prompts/#{URI.encode(name)}", params)
get("/api/public/v2/prompts/#{URI.encode(name, &URI.char_unreserved?/1)}", params)
end

@doc """
Expand All @@ -199,7 +205,7 @@ defmodule Langfuse.Client do
"""
@spec get_dataset(String.t()) :: response()
def get_dataset(name) do
get("/api/public/v2/datasets/#{URI.encode(name)}")
get("/api/public/v2/datasets/#{URI.encode(name, &URI.char_unreserved?/1)}")
end

@doc """
Expand Down Expand Up @@ -837,7 +843,7 @@ defmodule Langfuse.Client do
"""
@spec delete_dataset(String.t()) :: :ok | {:error, term()}
def delete_dataset(name) do
delete("/api/public/v2/datasets/#{URI.encode(name)}")
delete("/api/public/v2/datasets/#{URI.encode(name, &URI.char_unreserved?/1)}")
end

@doc """
Expand Down
6 changes: 4 additions & 2 deletions lib/langfuse/http.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,19 @@ defmodule Langfuse.HTTP do

* `:version` - Specific version number
* `:label` - Label to fetch (e.g., "production")
* `:resolve` - Whether to resolve prompt dependencies before returning (defaults to `true` on server)

"""
@impl true
@spec get_prompt(String.t(), keyword()) :: response()
def get_prompt(name, opts \\ []) do
params =
[name: name]
[]
|> maybe_add(:version, opts[:version])
|> maybe_add(:label, opts[:label])
|> maybe_add(:resolve, opts[:resolve])

get(@prompts_path, params)
get("#{@prompts_path}/#{URI.encode(name, &URI.char_unreserved?/1)}", params)
end

@doc """
Expand Down
21 changes: 14 additions & 7 deletions lib/langfuse/prompt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ defmodule Langfuse.Prompt do

* `:version` - Specific version number to fetch.
* `:label` - Label to fetch (e.g., "production", "latest").
* `:resolve` - Whether to resolve prompt dependencies before returning (defaults to `true` on server).
* `:cache_ttl` - Cache TTL in milliseconds. Defaults to 60,000 (1 minute).
* `:fallback` - Fallback prompt struct or template to use if fetch fails.
Can be a `%Langfuse.Prompt{}` struct or a string template.
Expand Down Expand Up @@ -188,6 +189,7 @@ defmodule Langfuse.Prompt do

* `:version` - Specific version number to fetch.
* `:label` - Label to fetch (e.g., "production", "latest").
* `:resolve` - Whether to resolve prompt dependencies before returning (defaults to `true` on server).

## Examples

Expand Down Expand Up @@ -324,10 +326,8 @@ defmodule Langfuse.Prompt do
"""
@spec invalidate(String.t(), keyword()) :: :ok
def invalidate(name, opts \\ []) do
key = cache_key(name, opts)

if opts[:version] || opts[:label] do
delete_cache_key(key)
delete_cache_entries(name, opts[:version], opts[:label])
else
delete_cache_by_name(name)
end
Expand Down Expand Up @@ -361,21 +361,28 @@ defmodule Langfuse.Prompt do
defp cache_key(name, opts) do
version = opts[:version]
label = opts[:label]
{name, version, label}
resolve = if(opts[:resolve] == false, do: false, else: true)
{name, version, label, resolve}
end

defp delete_cache_key(key) do
:ets.delete(:langfuse_prompt_cache, key)
defp delete_cache_entries(name, version, label) do
:ets.match_delete(
:langfuse_prompt_cache,
{{name, cache_match(version), cache_match(label), :_}, :_, :_}
)
rescue
ArgumentError -> :ok
end

defp delete_cache_by_name(name) do
:ets.match_delete(:langfuse_prompt_cache, {{name, :_, :_}, :_, :_})
:ets.match_delete(:langfuse_prompt_cache, {{name, :_, :_, :_}, :_, :_})
rescue
ArgumentError -> :ok
end

defp cache_match(nil), do: :_
defp cache_match(value), do: value

defp get_cached(key) do
case :ets.lookup(:langfuse_prompt_cache, key) do
[{^key, prompt, expires_at}] ->
Expand Down
28 changes: 20 additions & 8 deletions test/langfuse/http_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ defmodule Langfuse.HTTPTest do
end

test "get_prompt/2 fetches prompt by name", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts", fn conn ->
assert conn.query_string =~ "name=test-prompt"

Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts/test-prompt", fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.resp(
Expand All @@ -177,8 +175,7 @@ defmodule Langfuse.HTTPTest do
end

test "get_prompt/2 with version option", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts", fn conn ->
assert conn.query_string =~ "name=test-prompt"
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts/test-prompt", fn conn ->
assert conn.query_string =~ "version=3"

conn
Expand All @@ -194,8 +191,7 @@ defmodule Langfuse.HTTPTest do
end

test "get_prompt/2 with label option", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts", fn conn ->
assert conn.query_string =~ "name=test-prompt"
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts/test-prompt", fn conn ->
assert conn.query_string =~ "label=staging"

conn
Expand All @@ -210,8 +206,24 @@ defmodule Langfuse.HTTPTest do
assert "staging" in prompt["labels"]
end

test "get_prompt/2 with resolve option", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts/test-prompt", fn conn ->
assert conn.query_string =~ "resolve=false"

conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.resp(
200,
Jason.encode!(%{name: "test-prompt", version: 1, prompt: "raw {{dep}}"})
)
end)

assert {:ok, prompt} = Langfuse.HTTP.get_prompt("test-prompt", resolve: false)
assert prompt["prompt"] == "raw {{dep}}"
end

test "get_prompt/2 handles not found", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts", fn conn ->
Bypass.expect_once(bypass, "GET", "/api/public/v2/prompts/nonexistent", fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.resp(404, Jason.encode!(%{error: "Prompt not found"}))
Expand Down
Loading
Loading