diff --git a/langsmith-java-example/README.md b/langsmith-java-example/README.md index 11f072d2..3cc1b105 100644 --- a/langsmith-java-example/README.md +++ b/langsmith-java-example/README.md @@ -8,10 +8,15 @@ This module contains runnable examples organized by feature: All examples require: ```bash +./gradlew :langsmith-java-example:run -Pexample=ExampleName -Dlangchain.langsmithApiKey=your_api_key +``` + +Alternatively, you can use environment variables: +```bash export LANGSMITH_API_KEY=your_api_key ``` -The `LANGCHAIN_BASE_URL` environment variable is optional and defaults to `https://api.smith.langchain.com` if not set. +The `langchain.baseUrl` system property (or `LANGCHAIN_BASE_URL` environment variable) is optional and defaults to `https://api.smith.langchain.com/` if not set. ## OpenTelemetry Tracing Examples @@ -37,10 +42,10 @@ open http://localhost:16686 Make actual OpenAI API calls with automatic tracing to LangSmith. ```bash -export OPENAI_API_KEY=your_openai_key -export LANGSMITH_PROJECT=my-project # optional, defaults to "default" - -./gradlew :langsmith-java-example:run -Pexample=OtelOpenAI +./gradlew :langsmith-java-example:run -Pexample=OtelOpenAI \ + -Dlangchain.langsmithApiKey=your_api_key \ + -DOPENAI_API_KEY=your_openai_key \ + -DLANGSMITH_PROJECT=my-project # optional, defaults to "default" ``` View traces at https://smith.langchain.com @@ -50,9 +55,9 @@ View traces at https://smith.langchain.com Send mock traces to LangSmith without external API calls. ```bash -export LANGSMITH_PROJECT=my-project # optional, defaults to "default" - -./gradlew :langsmith-java-example:run -Pexample=OtelLangSmith +./gradlew :langsmith-java-example:run -Pexample=OtelLangSmith \ + -Dlangchain.langsmithApiKey=your_api_key \ + -DLANGSMITH_PROJECT=my-project # optional, defaults to "default" ``` View traces at https://smith.langchain.com @@ -62,10 +67,10 @@ View traces at https://smith.langchain.com REST API with OpenTelemetry traces sent to LangSmith. ```bash -export LANGSMITH_PROJECT=my-project # optional - # Start server -./gradlew :langsmith-java-example:run -Pexample=SpringBootLangSmith +./gradlew :langsmith-java-example:run -Pexample=SpringBootLangSmith \ + -Dlangchain.langsmithApiKey=your_api_key \ + -DLANGSMITH_PROJECT=my-project # optional # In another terminal, test endpoints: curl -X POST http://localhost:8080/api/chat \ @@ -84,7 +89,8 @@ Located in `src/main/java/com/langchain/smith/example/prompt/` **RECOMMENDED** - Clean, simple example following the same pattern as the Dataset example. ```bash -./gradlew :langsmith-java-example:run -Pexample=PromptManagement +./gradlew :langsmith-java-example:run -Pexample=PromptManagement \ + -Dlangchain.langsmithApiKey=your_api_key ``` **Features demonstrated:** diff --git a/langsmith-java-example/src/main/java/com/langchain/smith/example/prompt/PromptManagementExample.java b/langsmith-java-example/src/main/java/com/langchain/smith/example/prompt/PromptManagementExample.java deleted file mode 100644 index ceb49c29..00000000 --- a/langsmith-java-example/src/main/java/com/langchain/smith/example/prompt/PromptManagementExample.java +++ /dev/null @@ -1,374 +0,0 @@ -package com.langchain.smith.example.prompt; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.langchain.smith.client.LangsmithClient; -import com.langchain.smith.client.okhttp.LangsmithOkHttpClient; -import com.langchain.smith.core.JsonValue; -import com.langchain.smith.models.commits.CommitListParams; -import com.langchain.smith.models.commits.CommitListResponse; -import com.langchain.smith.models.commits.CommitManifestResponse; -import com.langchain.smith.models.commits.CommitRetrieveParams; -import com.langchain.smith.models.commits.CommitUpdateParams; -import com.langchain.smith.models.repos.RepoCreateParams; -import com.langchain.smith.models.repos.RepoListParams; -import com.langchain.smith.models.repos.RepoListResponse; -import com.langchain.smith.models.repos.RepoWithLookups; -import java.util.HashMap; -import java.util.Map; - -/** - * Demonstrates how to manage prompts programmatically using the LangSmith Java - * SDK. - * - *

- * This example shows: - * - Creating prompt repositories (repos) - * - Adding prompt content as commits (with variables) - * - Listing prompts with filters - * - Retrieving prompt content - * - Pulling specific versions - * - *

- * Prerequisites: - * - `LANGSMITH_API_KEY`: Your LangSmith API key - * - `LANGCHAIN_BASE_URL`: LangSmith API URL (https://api.smith.langchain.com) - * - *

- * Running: - * ```bash - * ./gradlew :langsmith-java-example:run -Pexample=PromptManagement - * ``` - */ -public class PromptManagementExample { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - - public static void main(String[] args) { - System.out.println("=== LangSmith Prompt Management Example ===\n"); - - LangsmithClient client = LangsmithOkHttpClient.fromEnv(); - String owner = getOwnerFromEnv(); - String promptName = "joke-generator"; - - try { - // 1. Check if prompt already exists - System.out.println("1. Checking for existing prompt using client.repos().list()..."); - RepoWithLookups existing = findPrompt(client, promptName); - boolean promptExists = existing != null; - - if (promptExists) { - System.out.println(" ℹ Prompt '" + promptName + "' already exists"); - System.out.println(" View: https://smith.langchain.com/prompts/" + existing.fullName()); - System.out.println(" Skipping creation steps...\n"); - } else { - System.out.println(" ✓ No existing prompt found\n"); - - // 2. Create prompt repository (metadata only) - System.out.println("2. Creating prompt repository using client.repos().create()..."); - client.repos() - .create(RepoCreateParams.builder() - .repoHandle(promptName) - .description("A joke generator that accepts a topic variable") - .isPublic(false) - .build()); - System.out.println(" ✓ Created prompt repository: " + promptName + "\n"); - } - - // 3. Add prompt content as a commit (only if prompt is new or has no commits) - boolean commitCreated = false; - if (promptExists) { - String parentCommitHash = getLatestCommitHash(client, promptName, owner); - if (parentCommitHash != null) { - System.out.println("3. Prompt already has content, skipping commit creation..."); - System.out.println(" ℹ To update the prompt, delete it first or modify the content\n"); - } else { - commitCreated = createCommit(client, promptName, owner, null); - } - } else { - commitCreated = createCommit(client, promptName, owner, null); - } - - // 4. List prompts - System.out.println("4. Listing prompts using client.repos().list()..."); - RepoListResponse allPrompts = client.repos() - .list(RepoListParams.builder() - .isPublic(RepoListParams.IsPublic.FALSE) - .limit(100L) - .build()); - System.out.println(" ✓ Found " + allPrompts.repos().size() + " prompt(s) in your organization"); - - RepoListResponse jokePrompts = client.repos() - .list(RepoListParams.builder() - .query("joke") - .isPublic(RepoListParams.IsPublic.FALSE) - .limit(100L) - .build()); - System.out.println( - " ✓ Found " + jokePrompts.repos().size() + " prompt(s) matching 'joke' in your organization\n"); - - // 5. Pull prompt content (retrieve latest commit) - System.out.println("5. Pulling prompt content using client.commits().list() and retrieve()..."); - - String commitHash = getLatestCommitHash(client, promptName, owner); - if (commitHash == null) { - throw new RuntimeException("No commits found for prompt"); - } - - System.out.println(" ✓ Latest commit: " + commitHash.substring(0, 8) + "..."); - - // Retrieve the manifest - CommitManifestResponse manifestResponse = client.commits() - .retrieve( - commitHash, - CommitRetrieveParams.builder() - .owner(owner) - .repo(promptName) - .build()); - - String promptContent = extractPromptContent(manifestResponse._manifest()); - System.out.println(" ✓ Prompt content: \"" + promptContent + "\"\n"); - - // 6. Demonstrate using the prompt with a value - System.out.println("6. Using the prompt with a value..."); - String filledPrompt = promptContent.replace("{topic}", "dogs"); - System.out.println(" Template: \"" + promptContent + "\""); - System.out.println(" Filled: \"" + filledPrompt + "\"\n"); - - // Summary - System.out.println("=== Summary ==="); - if (!promptExists) { - System.out.println("✓ Created prompt repository"); - } else { - System.out.println("ℹ Prompt already existed (skipped creation)"); - } - if (commitCreated) { - System.out.println("✓ Added prompt content as commit"); - } else { - System.out.println("ℹ Prompt content unchanged (skipped commit)"); - } - System.out.println("✓ Listed and filtered prompts"); - System.out.println("✓ Retrieved prompt content"); - System.out.println("✓ Demonstrated using prompt with a value"); - System.out.println(); - System.out.println("Learn more:"); - System.out.println("https://docs.langchain.com/langsmith/manage-prompts-programmatically"); - - } catch (Exception e) { - System.err.println("\nError: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); - } finally { - client.close(); - } - } - - /** - * Creates a chat prompt manifest with multiple messages (system + user) compatible with LangSmith. - */ - private static Map createChatPromptWithMultipleMessages( - String systemMessage, String userMessage, String[] inputVariables) { - Map manifest = new HashMap<>(); - manifest.put("lc", 1); - manifest.put("type", "constructor"); - manifest.put("id", new String[] {"langchain", "prompts", "chat", "ChatPromptTemplate"}); - - Map kwargs = new HashMap<>(); - kwargs.put("input_variables", inputVariables); - - java.util.List> messages = new java.util.ArrayList<>(); - messages.add(createMessagePrompt("system", systemMessage)); - messages.add(createMessagePrompt("human", userMessage)); - - kwargs.put("messages", messages.toArray()); - manifest.put("kwargs", kwargs); - - return manifest; - } - - /** - * Creates a message prompt (system, human, or AI). - */ - private static Map createMessagePrompt(String messageType, String template) { - String className; - switch (messageType.toLowerCase()) { - case "system": - className = "SystemMessagePromptTemplate"; - break; - case "ai": - case "assistant": - className = "AIMessagePromptTemplate"; - break; - case "human": - case "user": - default: - className = "HumanMessagePromptTemplate"; - break; - } - - Map message = new HashMap<>(); - message.put("lc", 1); - message.put("type", "constructor"); - message.put("id", new String[] {"langchain", "core", "prompts", "chat", className}); - - Map stringPrompt = new HashMap<>(); - stringPrompt.put("lc", 1); - stringPrompt.put("type", "prompt"); - stringPrompt.put("id", new String[] {"langchain", "prompts", "prompt", "StringPromptTemplate"}); - Map stringPromptKwargs = new HashMap<>(); - stringPromptKwargs.put("template", template); - stringPrompt.put("kwargs", stringPromptKwargs); - - Map msgKwargs = new HashMap<>(); - msgKwargs.put("prompt", stringPrompt); - message.put("kwargs", msgKwargs); - - return message; - } - - /** - * Determines message type from the message object's id field. - */ - @SuppressWarnings("unchecked") - private static String getMessageType(Map message) { - try { - Object idObj = message.get("id"); - String className = null; - - if (idObj instanceof Object[]) { - Object[] idArray = (Object[]) idObj; - if (idArray.length > 0) { - className = idArray[idArray.length - 1].toString(); - } - } else if (idObj instanceof java.util.List) { - java.util.List idList = (java.util.List) idObj; - if (!idList.isEmpty()) { - className = idList.get(idList.size() - 1).toString(); - } - } - - if (className != null) { - if (className.contains("System")) { - return "system"; - } else if (className.contains("AI") || className.contains("Assistant")) { - return "assistant"; - } - } - } catch (Exception e) { - // Default to "user" if we can't determine the type - } - return "user"; - } - - /** - * Finds a prompt by exact repo handle match. - * - * @return the prompt if found, null otherwise - */ - private static RepoWithLookups findPrompt(LangsmithClient client, String promptName) { - RepoListResponse existingRepos = client.repos() - .list(RepoListParams.builder() - .query(promptName) - .isPublic(RepoListParams.IsPublic.FALSE) - .build()); - - for (RepoWithLookups repo : existingRepos.repos()) { - if (repo.repoHandle().equals(promptName)) { - return repo; - } - } - return null; - } - - /** - * Helper method to get the latest commit hash for a prompt repository. - */ - private static String getLatestCommitHash(LangsmithClient client, String promptName, String owner) { - try { - CommitListResponse commits = client.commits() - .list( - promptName, - CommitListParams.builder() - .owner(owner) - .repo(promptName) - .limit(1L) - .build()); - if (!commits.commits().isEmpty()) { - return commits.commits().get(0).commitHash(); - } - } catch (Exception e) { - // No commits found - } - return null; - } - - /** - * Creates a commit with prompt content. - * - * @return true if commit was created, false otherwise - */ - private static boolean createCommit(LangsmithClient client, String promptName, String owner, String parentCommit) { - System.out.println("3. Adding prompt content using client.commits().update()..."); - Map manifest = createChatPromptWithMultipleMessages( - "You are a helpful assistant that tells jokes.", "Tell me a joke about {topic}", new String[] {"topic" - }); - - CommitUpdateParams.Builder builder = - CommitUpdateParams.builder().owner(owner).manifest(JsonValue.from(manifest)); - if (parentCommit != null) { - builder.parentCommit(parentCommit); - } - - try { - client.commits().update(promptName, builder.build()); - System.out.println(" ✓ Added prompt content as commit\n"); - return true; - } catch (Exception e) { - throw new RuntimeException("Failed to create commit", e); - } - } - - /** - * Extracts prompt content from a manifest for display (shows all messages). - */ - @SuppressWarnings("unchecked") - private static String extractPromptContent(JsonValue manifestJson) { - try { - Map manifest = MAPPER.convertValue(manifestJson, Map.class); - Map kwargs = (Map) manifest.get("kwargs"); - Object messagesObj = kwargs.get("messages"); - - Object[] messages; - if (messagesObj instanceof Object[]) { - messages = (Object[]) messagesObj; - } else if (messagesObj instanceof java.util.List) { - java.util.List messagesList = (java.util.List) messagesObj; - messages = messagesList.toArray(new Object[0]); - } else { - return "Unable to parse prompt content"; - } - - StringBuilder result = new StringBuilder(); - for (int i = 0; i < messages.length; i++) { - Map message = (Map) messages[i]; - Map msgKwargs = (Map) message.get("kwargs"); - Map prompt = (Map) msgKwargs.get("prompt"); - Map promptKwargs = (Map) prompt.get("kwargs"); - String template = promptKwargs.get("template").toString(); - String messageType = getMessageType(message); - - if (i > 0) { - result.append(" | "); - } - result.append(messageType).append(": \"").append(template).append("\""); - } - return result.toString(); - } catch (Exception e) { - return "Unable to parse prompt content"; - } - } - - private static String getOwnerFromEnv() { - String owner = System.getenv("LANGSMITH_OWNER"); - return (owner == null || owner.isEmpty()) ? "-" : owner; - } -} diff --git a/langsmith-java-example/src/main/kotlin/com/langchain/smith/example/PromptManagementExample.kt b/langsmith-java-example/src/main/kotlin/com/langchain/smith/example/PromptManagementExample.kt new file mode 100644 index 00000000..1aa201c7 --- /dev/null +++ b/langsmith-java-example/src/main/kotlin/com/langchain/smith/example/PromptManagementExample.kt @@ -0,0 +1,317 @@ +package com.langchain.smith.example + +import com.fasterxml.jackson.databind.ObjectMapper +import com.langchain.smith.client.LangsmithClient +import com.langchain.smith.client.okhttp.LangsmithOkHttpClient +import com.langchain.smith.core.JsonValue +import com.langchain.smith.models.commits.CommitManifestResponse +import com.langchain.smith.models.commits.CommitRetrieveParams +import com.langchain.smith.models.commits.CommitUpdateParams +import com.langchain.smith.models.repos.RepoCreateParams +import com.langchain.smith.models.repos.RepoListParams +import com.langchain.smith.models.repos.RepoWithLookups + +/** + * Demonstrates how to manage prompts programmatically using the LangSmith Java + * SDK. + * + * This example shows: + * - Creating prompt repositories (repos) + * - Adding prompt content as commits (with variables) + * - Listing prompts with filters + * - Retrieving prompt content + * - Pulling specific versions + * + * Prerequisites: + * - `LANGSMITH_API_KEY`: Your LangSmith API key + * - `LANGCHAIN_BASE_URL`: LangSmith API URL (https://api.smith.langchain.com) + * + * Running: + * ```bash + * ./gradlew :langsmith-java-example:run -Pexample=PromptManagement + * ``` + */ +private val MAPPER = ObjectMapper() + +fun main() { + println("=== LangSmith Prompt Management Example ===\n") + + val client = LangsmithOkHttpClient.fromEnv() + val owner = getOwnerFromEnv() + val promptName = "joke-generator" + + try { + // 1. Check if prompt already exists + println("1. Checking for existing prompt using client.repos().list()...") + val existing = findPrompt(client, promptName) + val promptExists = existing.isPresent + + if (promptExists) { + val repo = existing.get() + println("2. Prompt '$promptName' already exists") + println(" View: https://smith.langchain.com/prompts/${repo.fullName()}") + println(" Skipping creation steps...\n") + } else { + println(" ✓ No existing prompt found\n") + + // 2. Create prompt repository (metadata only) + println("2. Creating prompt repository using client.repos().create()...") + client.repos() + .create( + RepoCreateParams.builder() + .repoHandle(promptName) + .description("A joke generator that accepts a topic variable") + .isPublic(false) + .build() + ) + println(" ✓ Created prompt repository: $promptName\n") + } + + // 3. Add prompt content as a commit (only if prompt is new or has no commits) + val commitCreated = if (!promptExists || !hasLatestCommit(client, promptName, owner)) { + createCommit(client, promptName, owner, null) + } else { + println("3. Prompt already has content, skipping commit creation...") + println(" To update the prompt, delete it first or modify the content\n") + false + } + + // 4. List prompts + println("4. Listing prompts using client.repos().list()...") + val allPrompts = client.repos() + .list( + RepoListParams.builder() + .isPublic(RepoListParams.IsPublic.FALSE) + .build() + ) + println(" ✓ Found ${allPrompts.repos().size} prompt(s) in your organization") + + val jokePrompts = client.repos() + .list( + RepoListParams.builder() + .query("joke") + .isPublic(RepoListParams.IsPublic.FALSE) + .build() + ) + println(" ✓ Found ${jokePrompts.repos().size} prompt(s) matching 'joke' in your organization\n") + + // 5. Pull prompt content (retrieve latest commit) + println("5. Pulling prompt content using client.commits().retrieve() with commit='latest'...") + + // Retrieve the manifest using "latest" as the commit hash + val manifestResponse = client.commits() + .retrieve( + "latest", + CommitRetrieveParams.builder() + .owner(owner) + .repo(promptName) + .build() + ) + + println(" ✓ Retrieved latest commit manifest") + + val promptContent = extractPromptContent(manifestResponse._manifest()) + println(" ✓ Prompt content: \"$promptContent\"\n") + + // 6. Demonstrate using the prompt with a value + println("6. Using the prompt with a value...") + val filledPrompt = promptContent.replace("{topic}", "dogs") + println(" Template: \"$promptContent\"") + println(" Filled: \"$filledPrompt\"\n") + + // Summary + println("=== Summary ===") + if (!promptExists) { + println("✓ Created prompt repository") + } else { + println("ℹ Prompt already existed (skipped creation)") + } + if (commitCreated) { + println("✓ Added prompt content as commit") + } else { + println("ℹ Prompt content unchanged (skipped commit)") + } + println("✓ Listed and filtered prompts") + println("✓ Retrieved prompt content") + println("✓ Demonstrated using prompt with a value") + println() + println("Learn more:") + println("https://docs.langchain.com/langsmith/manage-prompts-programmatically") + + } catch (e: Exception) { + System.err.println("\nError: ${e.message}") + e.printStackTrace() + System.exit(1) + } finally { + client.close() + } +} + +/** + * Creates a chat prompt manifest with multiple messages (system + user) compatible with LangSmith. + */ +private fun createChatPromptWithMultipleMessages( + systemMessage: String, + userMessage: String, + inputVariables: Array +): Map { + val messages = listOf( + createMessagePrompt("system", systemMessage), + createMessagePrompt("human", userMessage) + ) + + return mapOf( + "lc" to 1, + "type" to "constructor", + "id" to arrayOf("langchain", "prompts", "chat", "ChatPromptTemplate"), + "kwargs" to mapOf( + "input_variables" to inputVariables, + "messages" to messages.toTypedArray() + ) + ) +} + +/** + * Creates a message prompt (system, human, or AI). + */ +private fun createMessagePrompt(messageType: String, template: String): Map { + val className = when (messageType.lowercase()) { + "system" -> "SystemMessagePromptTemplate" + "ai", "assistant" -> "AIMessagePromptTemplate" + "human", "user" -> "HumanMessagePromptTemplate" + else -> "HumanMessagePromptTemplate" + } + + val stringPrompt = mutableMapOf( + "lc" to 1, + "type" to "prompt", + "id" to arrayOf("langchain", "prompts", "prompt", "StringPromptTemplate"), + "kwargs" to mapOf("template" to template) + ) + + return mapOf( + "lc" to 1, + "type" to "constructor", + "id" to arrayOf("langchain", "core", "prompts", "chat", className), + "kwargs" to mapOf("prompt" to stringPrompt) + ) +} + +/** + * Determines message type from the message object's id field. + */ +@Suppress("UNCHECKED_CAST") +private fun getMessageType(message: Map): String { + return runCatching { + val idObj = message["id"] + val className = when (idObj) { + is Array<*> -> idObj.lastOrNull()?.toString() + is List<*> -> idObj.lastOrNull()?.toString() + else -> null + } + + when { + className?.contains("System") == true -> "system" + className?.contains("AI") == true || className?.contains("Assistant") == true -> "assistant" + else -> "user" + } + }.getOrDefault("user") +} + +/** + * Finds a prompt by exact repo handle match. + */ +private fun findPrompt(client: LangsmithClient, promptName: String): java.util.Optional { + val existingRepos = client.repos() + .list( + RepoListParams.builder() + .query(promptName) + .isPublic(RepoListParams.IsPublic.FALSE) + .build() + ) + + return existingRepos.repos().stream() + .filter { it.repoHandle() == promptName } + .findFirst() +} + +/** + * Checks if a prompt repository has a latest commit. + */ +private fun hasLatestCommit(client: LangsmithClient, promptName: String, owner: String): Boolean { + return runCatching { + client.commits().retrieve( + "latest", + CommitRetrieveParams.builder() + .owner(owner) + .repo(promptName) + .build() + ) + }.isSuccess +} + +/** + * Creates a commit with prompt content. + * + * @return true if commit was created, false otherwise + */ +private fun createCommit( + client: LangsmithClient, + promptName: String, + owner: String, + parentCommit: String? +): Boolean { + println("3. Adding prompt content using client.commits().update()...") + val manifest = createChatPromptWithMultipleMessages( + "You are a helpful assistant that tells jokes.", + "Tell me a joke about {topic}", + arrayOf("topic") + ) + + val params = CommitUpdateParams.builder() + .owner(owner) + .manifest(JsonValue.from(manifest)) + .apply { parentCommit?.let { parentCommit(it) } } + .build() + + return runCatching { + client.commits().update(promptName, params) + println(" ✓ Added prompt content as commit\n") + true + }.getOrElse { e -> + throw RuntimeException("Failed to create commit", e) + } +} + +/** + * Extracts prompt content from a manifest for display (shows all messages). + */ +@Suppress("UNCHECKED_CAST") +private fun extractPromptContent(manifestJson: JsonValue): String { + return runCatching { + val manifest = MAPPER.convertValue(manifestJson, Map::class.java) as Map + val kwargs = manifest["kwargs"] as Map + val messagesObj = kwargs["messages"] ?: return "Unable to parse prompt content" + + val messages = when (messagesObj) { + is Array<*> -> messagesObj + is List<*> -> messagesObj.toTypedArray() + else -> return "Unable to parse prompt content" + } + + messages.mapIndexed { index, messageObj -> + val message = messageObj as Map + val msgKwargs = message["kwargs"] as Map + val prompt = msgKwargs["prompt"] as Map + val promptKwargs = prompt["kwargs"] as Map + val template = promptKwargs["template"].toString() + val messageType = getMessageType(message) + val prefix = if (index > 0) " | " else "" + "$prefix$messageType: \"$template\"" + }.joinToString("") + }.getOrDefault("Unable to parse prompt content") +} + +private fun getOwnerFromEnv(): String = + System.getenv("LANGSMITH_OWNER")?.takeIf { it.isNotEmpty() } ?: "-" +