diff --git a/DSL/CronManager/DSL/store_in_vault.yml b/DSL/CronManager/DSL/store_in_vault.yml index 63522a1..3052219 100644 --- a/DSL/CronManager/DSL/store_in_vault.yml +++ b/DSL/CronManager/DSL/store_in_vault.yml @@ -2,4 +2,4 @@ store_secrets: trigger: off type: exec command: "/app/scripts/store_secrets_in_vault.sh" - allowedEnvs: ['cookie', 'connectionId','llmPlatform', 'llmModel','secretKey','accessKey','deploymentName','targetUrl','apiKey','embeddingModel','embeddingPlatform','embeddingModelApiKey','deploymentEnvironment'] \ No newline at end of file + allowedEnvs: ['cookie', 'connectionId','llmPlatform', 'llmModel','secretKey','accessKey','deploymentName','targetUrl','apiKey','embeddingModel','embeddingPlatform','embeddingAccessKey','embeddingSecretKey','embeddingDeploymentName','embeddingTargetUri','embeddingAzureApiKey','deploymentEnvironment'] \ No newline at end of file diff --git a/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql b/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql index 55cccd2..6367462 100644 --- a/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql +++ b/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql @@ -1,39 +1,40 @@ -- Schema for LLM Connections CREATE TABLE llm_connections ( + -- Metadata id SERIAL PRIMARY KEY, connection_name VARCHAR(255) NOT NULL DEFAULT '', - -- LLM Model Configuration - llm_platform VARCHAR(100) NOT NULL, -- e.g. Azure AI, OpenAI - llm_model VARCHAR(100) NOT NULL, -- e.g. GPT-4o - - -- Embedding Model Configuration - embedding_platform VARCHAR(100) NOT NULL, -- e.g. Azure AI, OpenAI - embedding_model VARCHAR(100) NOT NULL, -- e.g. Ada-200-1 - - -- Budget and Usage Tracking - monthly_budget NUMERIC(12,2) NOT NULL, -- e.g. 1000.00 - used_budget NUMERIC(12,2) DEFAULT 0.00, -- e.g. 250.00 - warn_budget_threshold NUMERIC(5) DEFAULT 80, -- percentage to warn at - stop_budget_threshold NUMERIC(5) DEFAULT 100, -- percentage to stop at - disconnect_on_budget_exceed BOOLEAN DEFAULT TRUE, - - -- Metadata connection_status VARCHAR(50) DEFAULT 'active', -- active / inactive created_at TIMESTAMP DEFAULT NOW(), environment VARCHAR(50) NOT NULL, - -- Mocked Credentials and Access Info + -- LLM Model Configuration + llm_platform VARCHAR(100) NOT NULL, -- e.g. Azure AI, OpenAI + llm_model VARCHAR(100) NOT NULL, -- e.g. GPT-4o -- Azure deployment_name VARCHAR(150), -- for Azure deployments target_uri TEXT, -- for custom endpoints api_key TEXT, -- secured api key mocked here - -- AWS Bedrock secret_key TEXT, access_key TEXT, - -- Embedding Model - embedding_model_api_key TEXT + -- Embedding Model Configuration + embedding_platform VARCHAR(100) NOT NULL, -- e.g. Azure AI, OpenAI + embedding_model VARCHAR(100) NOT NULL, -- e.g. Ada-200-1 + -- Azure + embedding_deployment_name VARCHAR(150), -- for Azure deployments + embedding_target_uri TEXT, -- for custom endpoints + embedding_azure_api_key TEXT, -- secured api key mocked here + -- AWS Bedrock + embedding_secret_key TEXT, + embedding_access_key TEXT, + + -- Budget and Usage Tracking + monthly_budget NUMERIC(12,2) NOT NULL, -- e.g. 1000.00 + used_budget NUMERIC(12,2) DEFAULT 0.00, -- e.g. 250.00 + warn_budget_threshold NUMERIC(5) DEFAULT 80, -- percentage to warn at + stop_budget_threshold NUMERIC(5) DEFAULT 100, -- percentage to stop at + disconnect_on_budget_exceed BOOLEAN DEFAULT TRUE ); CREATE TABLE inference_results ( diff --git a/DSL/Resql/rag-search/POST/get-llm-connection.sql b/DSL/Resql/rag-search/POST/get-llm-connection.sql index 30fdb93..a1128df 100644 --- a/DSL/Resql/rag-search/POST/get-llm-connection.sql +++ b/DSL/Resql/rag-search/POST/get-llm-connection.sql @@ -21,7 +21,11 @@ SELECT secret_key, access_key, -- Embedding model credentials - embedding_model_api_key + embedding_access_key, + embedding_secret_key, + embedding_deployment_name, + embedding_target_uri, + embedding_azure_api_key FROM llm_connections WHERE id = :connection_id AND connection_status <> 'deleted'; diff --git a/DSL/Resql/rag-search/POST/get-llm-connections-paginated.sql b/DSL/Resql/rag-search/POST/get-llm-connections-paginated.sql index 419d7bc..faf1600 100644 --- a/DSL/Resql/rag-search/POST/get-llm-connections-paginated.sql +++ b/DSL/Resql/rag-search/POST/get-llm-connections-paginated.sql @@ -14,7 +14,6 @@ SELECT connection_status, created_at, CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS totalPages, - -- Calculate budget status based on usage percentage and configured thresholds CASE WHEN used_budget IS NULL OR used_budget = 0 OR (used_budget::DECIMAL / monthly_budget::DECIMAL) < (warn_budget_threshold::DECIMAL / 100.0) THEN 'within_budget' WHEN stop_budget_threshold != 0 AND (used_budget::DECIMAL / monthly_budget::DECIMAL) >= (stop_budget_threshold::DECIMAL / 100.0) THEN 'over_budget' diff --git a/DSL/Resql/rag-search/POST/get-production-connection-filtered.sql b/DSL/Resql/rag-search/POST/get-production-connection-filtered.sql new file mode 100644 index 0000000..4d5ced0 --- /dev/null +++ b/DSL/Resql/rag-search/POST/get-production-connection-filtered.sql @@ -0,0 +1,54 @@ +SELECT + id, + connection_name, + llm_platform, + llm_model, + embedding_platform, + embedding_model, + monthly_budget, + warn_budget_threshold, + stop_budget_threshold, + disconnect_on_budget_exceed, + used_budget, + environment, + connection_status, + created_at, + deployment_name, + target_uri, + api_key, + secret_key, + access_key, + embedding_secret_key, + embedding_access_key, + embedding_deployment_name, + embedding_target_uri, + embedding_azure_api_key, + -- Calculate budget status based on usage percentage and configured thresholds + CASE + WHEN used_budget IS NULL OR used_budget = 0 OR (used_budget::DECIMAL / monthly_budget::DECIMAL) < (warn_budget_threshold::DECIMAL / 100.0) THEN 'within_budget' + WHEN stop_budget_threshold != 0 AND (used_budget::DECIMAL / monthly_budget::DECIMAL) >= (stop_budget_threshold::DECIMAL / 100.0) THEN 'over_budget' + WHEN stop_budget_threshold = 0 AND (used_budget::DECIMAL / monthly_budget::DECIMAL) >= 1 THEN 'over_budget' + WHEN (used_budget::DECIMAL / monthly_budget::DECIMAL) >= (warn_budget_threshold::DECIMAL / 100.0) THEN 'close_to_exceed' + ELSE 'within_budget' + END AS budget_status +FROM llm_connections +WHERE environment = 'production' + AND connection_status <> 'deleted' + AND (:llm_platform IS NULL OR :llm_platform = '' OR llm_platform = :llm_platform) + AND (:llm_model IS NULL OR :llm_model = '' OR llm_model = :llm_model) + AND (:embedding_platform IS NULL OR :embedding_platform = '' OR embedding_platform = :embedding_platform) + AND (:embedding_model IS NULL OR :embedding_model = '' OR embedding_model = :embedding_model) + AND (:connection_status IS NULL OR :connection_status = '' OR connection_status = :connection_status) +ORDER BY + CASE WHEN :sorting = 'connection_name asc' THEN connection_name END ASC, + CASE WHEN :sorting = 'connection_name desc' THEN connection_name END DESC, + CASE WHEN :sorting = 'llm_platform asc' THEN llm_platform END ASC, + CASE WHEN :sorting = 'llm_platform desc' THEN llm_platform END DESC, + CASE WHEN :sorting = 'llm_model asc' THEN llm_model END ASC, + CASE WHEN :sorting = 'llm_model desc' THEN llm_model END DESC, + CASE WHEN :sorting = 'monthly_budget asc' THEN monthly_budget END ASC, + CASE WHEN :sorting = 'monthly_budget desc' THEN monthly_budget END DESC, + CASE WHEN :sorting = 'created_at asc' THEN created_at END ASC, + CASE WHEN :sorting = 'created_at desc' THEN created_at END DESC, + created_at DESC -- Default fallback sorting +LIMIT 1; diff --git a/DSL/Resql/rag-search/POST/insert-llm-connection.sql b/DSL/Resql/rag-search/POST/insert-llm-connection.sql index c4d9679..29465ce 100644 --- a/DSL/Resql/rag-search/POST/insert-llm-connection.sql +++ b/DSL/Resql/rag-search/POST/insert-llm-connection.sql @@ -16,7 +16,11 @@ INSERT INTO llm_connections ( api_key, secret_key, access_key, - embedding_model_api_key + embedding_access_key, + embedding_secret_key, + embedding_deployment_name, + embedding_target_uri, + embedding_azure_api_key ) VALUES ( :connection_name, :llm_platform, @@ -35,7 +39,11 @@ INSERT INTO llm_connections ( :api_key, :secret_key, :access_key, - :embedding_model_api_key + :embedding_access_key, + :embedding_secret_key, + :embedding_deployment_name, + :embedding_target_uri, + :embedding_azure_api_key ) RETURNING id, connection_name, @@ -55,4 +63,8 @@ INSERT INTO llm_connections ( api_key, secret_key, access_key, - embedding_model_api_key; + embedding_secret_key, + embedding_access_key, + embedding_deployment_name, + embedding_target_uri, + embedding_azure_api_key diff --git a/DSL/Resql/rag-search/POST/update-llm-connection-environment.sql b/DSL/Resql/rag-search/POST/update-llm-connection-environment.sql index c16b98c..5b894c9 100644 --- a/DSL/Resql/rag-search/POST/update-llm-connection-environment.sql +++ b/DSL/Resql/rag-search/POST/update-llm-connection-environment.sql @@ -20,5 +20,4 @@ RETURNING target_uri, api_key, secret_key, - access_key, - embedding_model_api_key; + access_key; diff --git a/DSL/Resql/rag-search/POST/update-llm-connection-status.sql b/DSL/Resql/rag-search/POST/update-llm-connection-status.sql index 463936e..f71194a 100644 --- a/DSL/Resql/rag-search/POST/update-llm-connection-status.sql +++ b/DSL/Resql/rag-search/POST/update-llm-connection-status.sql @@ -19,5 +19,4 @@ RETURNING target_uri, api_key, secret_key, - access_key, - embedding_model_api_key; + access_key; diff --git a/DSL/Resql/rag-search/POST/update-llm-connection.sql b/DSL/Resql/rag-search/POST/update-llm-connection.sql index a442227..e4fa4fd 100644 --- a/DSL/Resql/rag-search/POST/update-llm-connection.sql +++ b/DSL/Resql/rag-search/POST/update-llm-connection.sql @@ -18,7 +18,12 @@ SET secret_key = :secret_key, access_key = :access_key, -- Embedding model credentials - embedding_model_api_key = :embedding_model_api_key + -- Embedding platform specific credentials + embedding_access_key = :embedding_access_key, + embedding_secret_key = :embedding_secret_key, + embedding_deployment_name = :embedding_deployment_name, + embedding_target_uri = :embedding_target_uri, + embedding_azure_api_key = :embedding_azure_api_key WHERE id = :connection_id RETURNING id, @@ -39,4 +44,8 @@ RETURNING api_key, secret_key, access_key, - embedding_model_api_key; + embedding_secret_key, + embedding_access_key, + embedding_deployment_name, + embedding_target_uri, + embedding_azure_api_key; diff --git a/DSL/Ruuter.private/rag-search/GET/llm-connections/production.yml b/DSL/Ruuter.private/rag-search/GET/llm-connections/production.yml index be75219..b64e046 100644 --- a/DSL/Ruuter.private/rag-search/GET/llm-connections/production.yml +++ b/DSL/Ruuter.private/rag-search/GET/llm-connections/production.yml @@ -1,15 +1,57 @@ declaration: call: declare version: 0.1 - description: "Get production LLM connection" + description: "Get production LLM connection with optional filters" method: get returns: json namespace: rag-search + allowlist: + params: + - field: llmPlatform + type: string + description: "Filter by LLM platform" + - field: llmModel + type: string + description: "Filter by LLM model" + - field: embeddingPlatform + type: string + description: "Filter by embedding platform" + - field: embeddingModel + type: string + description: "Filter by embedding model" + - field: connectionStatus + type: string + description: "Filter by connection status" + - field: sortBy + type: string + description: "Field to sort by" + - field: sortOrder + type: string + description: "Sort order: 'asc' or 'desc'" + +extract_request_data: + assign: + llmPlatform: ${incoming.params.llmPlatform ?? ""} + llmModel: ${incoming.params.llmModel ?? ""} + embeddingPlatform: ${incoming.params.embeddingPlatform ?? ""} + embeddingModel: ${incoming.params.embeddingModel ?? ""} + connectionStatus: ${incoming.params.connectionStatus ?? ""} + sortBy: ${incoming.params.sortBy ?? "created_at"} + sortOrder: ${incoming.params.sortOrder ?? "desc"} + sorting: ${sortBy + " " + sortOrder} + next: get_production_connection get_production_connection: call: http.post args: - url: "[#RAG_SEARCH_RESQL]/get-production-connection" + url: "[#RAG_SEARCH_RESQL]/get-production-connection-filtered" + body: + llm_platform: ${llmPlatform} + llm_model: ${llmModel} + embedding_platform: ${embeddingPlatform} + embedding_model: ${embeddingModel} + connection_status: ${connectionStatus} + sorting: ${sorting} result: connection_result next: return_success diff --git a/DSL/Ruuter.private/rag-search/POST/llm-connections/add.yml b/DSL/Ruuter.private/rag-search/POST/llm-connections/add.yml index dffe487..5e7326a 100644 --- a/DSL/Ruuter.private/rag-search/POST/llm-connections/add.yml +++ b/DSL/Ruuter.private/rag-search/POST/llm-connections/add.yml @@ -56,9 +56,23 @@ declaration: type: string description: "AWS access key" # Embedding model credentials - - field: embedding_model_api_key + # Embedding AWS Bedrock credentials + - field: embedding_access_key type: string - description: "Embedding model API key" + description: "AWS access key for embedding model" + - field: embedding_secret_key + type: string + description: "AWS secret key for embedding model" + # Embedding Azure credentials + - field: embedding_deployment_name + type: string + description: "Azure embedding deployment name" + - field: embedding_target_uri + type: string + description: "Azure embedding endpoint URI" + - field: embedding_azure_api_key + type: string + description: "Azure embedding API key" extract_request_data: assign: @@ -77,7 +91,12 @@ extract_request_data: api_key: ${incoming.body.api_key || ""} secret_key: ${incoming.body.secret_key || ""} access_key: ${incoming.body.access_key || ""} - embedding_model_api_key: ${incoming.body.embedding_model_api_key || ""} + # Embedding platform specific credentials + embedding_access_key: ${incoming.body.embedding_access_key || ""} + embedding_secret_key: ${incoming.body.embedding_secret_key || ""} + embedding_deployment_name: ${incoming.body.embedding_deployment_name || ""} + embedding_target_uri: ${incoming.body.embedding_target_uri || ""} + embedding_azure_api_key: ${incoming.body.embedding_azure_api_key || ""} created_at: ${new Date().toISOString()} next: validate_environment @@ -138,7 +157,12 @@ add_llm_connection: api_key: ${api_key} secret_key: ${secret_key} access_key: ${access_key} - embedding_model_api_key: ${embedding_model_api_key} + # Embedding platform specific credentials + embedding_access_key: ${embedding_access_key} + embedding_secret_key: ${embedding_secret_key} + embedding_deployment_name: ${embedding_deployment_name} + embedding_target_uri: ${embedding_target_uri} + embedding_azure_api_key: ${embedding_azure_api_key} result: connection_result next: assign_connection_response diff --git a/DSL/Ruuter.private/rag-search/POST/llm-connections/edit.yml b/DSL/Ruuter.private/rag-search/POST/llm-connections/edit.yml index 420f3ca..84b375d 100644 --- a/DSL/Ruuter.private/rag-search/POST/llm-connections/edit.yml +++ b/DSL/Ruuter.private/rag-search/POST/llm-connections/edit.yml @@ -56,9 +56,23 @@ declaration: - field: access_key type: string description: "AWS access key" - - field: embedding_model_api_key + # Embedding AWS Bedrock credentials + - field: embedding_access_key type: string - description: "Embedding model API key" + description: "AWS access key for embedding model" + - field: embedding_secret_key + type: string + description: "AWS secret key for embedding model" + # Embedding Azure credentials + - field: embedding_deployment_name + type: string + description: "Azure embedding deployment name" + - field: embedding_target_uri + type: string + description: "Azure embedding endpoint URI" + - field: embedding_azure_api_key + type: string + description: "Azure embedding API key" extract_request_data: assign: @@ -78,7 +92,12 @@ extract_request_data: api_key: ${incoming.body.api_key || ""} secret_key: ${incoming.body.secret_key || ""} access_key: ${incoming.body.access_key || ""} - embedding_model_api_key: ${incoming.body.embedding_model_api_key || ""} + # Embedding platform specific credentials + embedding_access_key: ${incoming.body.embedding_access_key || ""} + embedding_secret_key: ${incoming.body.embedding_secret_key || ""} + embedding_deployment_name: ${incoming.body.embedding_deployment_name || ""} + embedding_target_uri: ${incoming.body.embedding_target_uri || ""} + embedding_azure_api_key: ${incoming.body.embedding_azure_api_key || ""} updated_at: ${new Date().toISOString()} next: validate_environment @@ -124,7 +143,12 @@ update_llm_connection: api_key: ${api_key} secret_key: ${secret_key} access_key: ${access_key} - embedding_model_api_key: ${embedding_model_api_key} + # Embedding platform specific credentials + embedding_access_key: ${embedding_access_key} + embedding_secret_key: ${embedding_secret_key} + embedding_deployment_name: ${embedding_deployment_name} + embedding_target_uri: ${embedding_target_uri} + embedding_azure_api_key: ${embedding_azure_api_key} result: connection_result next: return_success diff --git a/DSL/Ruuter.private/rag-search/POST/vault/secret/create.yml b/DSL/Ruuter.private/rag-search/POST/vault/secret/create.yml index e05d015..96501b3 100644 --- a/DSL/Ruuter.private/rag-search/POST/vault/secret/create.yml +++ b/DSL/Ruuter.private/rag-search/POST/vault/secret/create.yml @@ -38,9 +38,23 @@ declaration: - field: embeddingPlatform type: string description: "Body field 'embeddingPlatform'" - - field: embeddingModelApiKey + # Embedding AWS Bedrock credentials + - field: embeddingAccessKey type: string - description: "Body field 'embeddingModelApiKey'" + description: "Body field 'embeddingAccessKey'" + - field: embeddingSecretKey + type: string + description: "Body field 'embeddingSecretKey'" + # Embedding Azure credentials + - field: embeddingDeploymentName + type: string + description: "Body field 'embeddingDeploymentName'" + - field: embeddingTargetUri + type: string + description: "Body field 'embeddingTargetUri'" + - field: embeddingAzureApiKey + type: string + description: "Body field 'embeddingAzureApiKey'" - field: deploymentEnvironment type: string description: "Body field 'deploymentEnvironment'" @@ -61,7 +75,13 @@ extract_request_data: apiKey: ${incoming.body.apiKey} embeddingModel: ${incoming.body.embeddingModel} embeddingPlatform: ${incoming.body.embeddingPlatform} - embeddingModelApiKey: ${incoming.body.embeddingModelApiKey} + # Embedding AWS Bedrock credentials + embeddingAccessKey: ${incoming.body.embeddingAccessKey} + embeddingSecretKey: ${incoming.body.embeddingSecretKey} + # Embedding Azure credentials + embeddingDeploymentName: ${incoming.body.embeddingDeploymentName} + embeddingTargetUri: ${incoming.body.embeddingTargetUri} + embeddingAzureApiKey: ${incoming.body.embeddingAzureApiKey} deploymentEnvironment: ${incoming.body.deploymentEnvironment} cookie: ${incoming.headers.cookie} next: check_provider @@ -87,7 +107,9 @@ execute_aws_request: accessKey: ${accessKey} embeddingModel: ${embeddingModel} embeddingPlatform: ${embeddingPlatform} - embeddingModelApiKey: ${embeddingModelApiKey} + # Embedding AWS Bedrock credentials + embeddingAccessKey: ${embeddingAccessKey} + embeddingSecretKey: ${embeddingSecretKey} deploymentEnvironment: ${deploymentEnvironment} result: cron_aws_res next: return_aws_ok @@ -106,7 +128,10 @@ execute_azure_request: apiKey: ${apiKey} embeddingModel: ${embeddingModel} embeddingPlatform: ${embeddingPlatform} - embeddingModelApiKey: ${embeddingModelApiKey} + # Embedding Azure credentials + embeddingDeploymentName: ${embeddingDeploymentName} + embeddingTargetUri: ${embeddingTargetUri} + embeddingAzureApiKey: ${embeddingAzureApiKey} deploymentEnvironment: ${deploymentEnvironment} result: cron_azure_res next: return_azure_ok diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index 2c7d73b..2ae1c70 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -34,6 +34,12 @@ const MainNavigation: FC = () => { label: 'Test LLM', path: '/test-llm', icon: + }, + { + id: 'testProductionLLM', + label: 'Test Production LLM', + path: '/test-production-llm', + icon: } ]; diff --git a/GUI/src/components/molecules/LLMConnectionForm/index.tsx b/GUI/src/components/molecules/LLMConnectionForm/index.tsx index 04557de..a86e7bd 100644 --- a/GUI/src/components/molecules/LLMConnectionForm/index.tsx +++ b/GUI/src/components/molecules/LLMConnectionForm/index.tsx @@ -38,6 +38,13 @@ export type LLMConnectionFormData = { apiKey?: string; // Embedding model credentials embeddingModelApiKey?: string; + // Embedding AWS Bedrock credentials + embeddingAccessKey?: string; + embeddingSecretKey?: string; + // Embedding Azure credentials + embeddingDeploymentName?: string; + embeddingTargetUri?: string; + embeddingAzureApiKey?: string; }; type LLMConnectionFormProps = { @@ -71,7 +78,6 @@ const LLMConnectionForm: React.FC = ({ llmModel: '', embeddingModelPlatform: '', embeddingModel: '', - embeddingModelApiKey: '', monthlyBudget: '', warnBudget: '', stopBudget: '', @@ -85,6 +91,14 @@ const LLMConnectionForm: React.FC = ({ targetUri: '', apiKey: '', // Embedding model credentials + embeddingModelApiKey: '', + // Embedding AWS Bedrock credentials + embeddingAccessKey: '', + embeddingSecretKey: '', + // Embedding Azure credentials + embeddingDeploymentName: '', + embeddingTargetUri: '', + embeddingAzureApiKey: '', ...defaultValues, }, mode: 'onChange', @@ -126,6 +140,10 @@ const embeddingModelOptions = toOptions(embeddingModelsData); const [secretKeyReplaceMode, setSecretKeyReplaceMode] = React.useState(isEditing); const [accessKeyReplaceMode, setAccessKeyReplaceMode] = React.useState(isEditing); const [embeddingApiKeyReplaceMode, setEmbeddingApiKeyReplaceMode] = React.useState(isEditing); + // Embedding platform specific replace modes + const [embeddingSecretKeyReplaceMode, setEmbeddingSecretKeyReplaceMode] = React.useState(isEditing); + const [embeddingAccessKeyReplaceMode, setEmbeddingAccessKeyReplaceMode] = React.useState(isEditing); + const [embeddingAzureApiKeyReplaceMode, setEmbeddingAzureApiKeyReplaceMode] = React.useState(isEditing); const resetLLMCredentialFields = () => { setValue('accessKey', ''); @@ -144,9 +162,18 @@ const embeddingModelOptions = toOptions(embeddingModelsData); const resetEmbeddingModelCredentialFields = () => { setValue('embeddingModelApiKey', ''); setValue('embeddingModel', ''); + // Reset embedding platform specific fields + setValue('embeddingAccessKey', ''); + setValue('embeddingSecretKey', ''); + setValue('embeddingDeploymentName', ''); + setValue('embeddingTargetUri', ''); + setValue('embeddingAzureApiKey', ''); - // Reset replace mode state when platform changes + // Reset replace mode states when platform changes setEmbeddingApiKeyReplaceMode(false); + setEmbeddingSecretKeyReplaceMode(false); + setEmbeddingAccessKeyReplaceMode(false); + setEmbeddingAzureApiKeyReplaceMode(false); }; // Model options based on selected platform const getLLMModelOptions = () => { @@ -315,6 +342,165 @@ const embeddingModelOptions = toOptions(embeddingModelsData); } }; + const renderEmbeddingPlatformSpecificFields = () => { + switch (selectedEmbeddingPlatform) { + case 'aws': + return ( + <> +
+

Embedding Access Key

+

AWS Access Key for Bedrock embedding service

+ ( + { + setEmbeddingAccessKeyReplaceMode(false); + setValue('embeddingAccessKey', ''); + }} + endButtonText="Change" + {...field} + /> + )} + /> +
+
+

Embedding Secret Key

+

AWS Secret Key for Bedrock embedding service

+ ( + { + setEmbeddingSecretKeyReplaceMode(false); + setValue('embeddingSecretKey', ''); + }} + endButtonText="Change" + {...field} + /> + )} + /> +
+ + ); + case 'azure': + return ( + <> +
+

Embedding Deployment Name

+

Azure OpenAI embedding deployment name

+ ( + + )} + /> +
+
+

Embedding Endpoint / Target URI

+

Azure OpenAI embedding service endpoint URL

+ ( + + )} + /> +
+
+

Embedding API Key

+

Azure OpenAI embedding API key

+ ( + { + setEmbeddingAzureApiKeyReplaceMode(false); + setValue('embeddingAzureApiKey', ''); + }} + endButtonText="Change" + {...field} + /> + )} + /> +
+ + ); + + default: + return ( +
+

Embedding Model API Key

+

API key of your embedding model

+ ( + { + setEmbeddingApiKeyReplaceMode(false); + setValue('embeddingModelApiKey', ''); + }} + endButtonText="Change" + {...field} + /> + )} + /> +
+ ); + } + }; + const handleFormSubmit = (data: LLMConnectionFormData) => { const cleanedData = { ...data, @@ -395,7 +581,7 @@ const embeddingModelOptions = toOptions(embeddingModelsData); options={getLLMModelOptions() || []} placeholder={ llmModelsLoading - ? "Loading models..." + ? "Select LLM Model" : llmModelsError ? "Error loading models" : !selectedLLMPlatform @@ -467,7 +653,7 @@ const embeddingModelOptions = toOptions(embeddingModelsData); options={getEmbeddingModelOptions() || []} placeholder={ embeddingModelsLoading - ? "Loading models..." + ? "Select Embedding Model" : embeddingModelsError ? "Error loading models" : !selectedEmbeddingPlatform @@ -486,32 +672,8 @@ const embeddingModelOptions = toOptions(embeddingModelsData); /> -
-

Embedding Model API Key

-

API key of your embedding model

- - ( - { - setEmbeddingApiKeyReplaceMode(false); - setValue('embeddingModelApiKey', ''); - }} - endButtonText="Change" - {...field} - /> - )} - /> -
+ {/* Embedding Platform-specific fields */} + {renderEmbeddingPlatformSpecificFields()}
diff --git a/GUI/src/pages/LLMConnections/CreateLLMConnection.tsx b/GUI/src/pages/LLMConnections/CreateLLMConnection.tsx index dfeb583..c77bdfc 100644 --- a/GUI/src/pages/LLMConnections/CreateLLMConnection.tsx +++ b/GUI/src/pages/LLMConnections/CreateLLMConnection.tsx @@ -15,8 +15,8 @@ const CreateLLMConnection = () => { // Query to check for existing production connection const { data: existingProductionConnection } = useQuery({ - queryKey: ['production-connection'], - queryFn: getProductionConnection, + queryKey: llmConnectionsQueryKeys.production(), + queryFn: () => getProductionConnection(), }); const createConnectionMutation = useMutation({ diff --git a/GUI/src/pages/LLMConnections/ViewLLMConnection.tsx b/GUI/src/pages/LLMConnections/ViewLLMConnection.tsx index 28e429f..3a55528 100644 --- a/GUI/src/pages/LLMConnections/ViewLLMConnection.tsx +++ b/GUI/src/pages/LLMConnections/ViewLLMConnection.tsx @@ -249,6 +249,13 @@ const ViewLLMConnection = () => { accessKey: connectionData.accessKey || '', // Don't show API keys // Embedding model credentials (don't show sensitive data, but include structure) embeddingModelApiKey: connectionData.embeddingModelApiKey || '', // Don't show API keys + // Embedding AWS Bedrock credentials + embeddingAccessKey: connectionData.embeddingAccessKey || '', + embeddingSecretKey: connectionData.embeddingSecretKey || '', + // Embedding Azure credentials + embeddingDeploymentName: connectionData.embeddingDeploymentName || '', + embeddingTargetUri: connectionData.embeddingTargetUri || '', + embeddingAzureApiKey: connectionData.embeddingAzureApiKey || '', }; return ( diff --git a/GUI/src/pages/LLMConnections/index.tsx b/GUI/src/pages/LLMConnections/index.tsx index 6d46024..18dd7f4 100644 --- a/GUI/src/pages/LLMConnections/index.tsx +++ b/GUI/src/pages/LLMConnections/index.tsx @@ -13,7 +13,7 @@ import BudgetBanner from 'components/molecules/BudgetBanner'; import './LLMConnections.scss'; import { platforms, trainingStatuses } from 'config/dataModelsConfig'; import LLMConnectionCard from 'components/molecules/LLMConnectionCard'; -import { fetchLLMConnectionsPaginated, LLMConnectionFilters, LLMConnection, getProductionConnection } from 'services/llmConnections'; +import { fetchLLMConnectionsPaginated, LLMConnectionFilters, LLMConnection, getProductionConnection, ProductionConnectionFilters } from 'services/llmConnections'; import { llmConnectionsQueryKeys } from 'utils/queryKeys'; const LLMConnections: FC = () => { @@ -35,10 +35,17 @@ const LLMConnections: FC = () => { queryFn: () => fetchLLMConnectionsPaginated(filters), }); - // Fetch production connection separately + // Fetch production connection separately with potential filters + const [productionFilters, setProductionFilters] = useState({ + sortBy: 'created_at', + sortOrder: 'desc', + llmPlatform: '', + llmModel: '', + }); + const { data: productionConnection, isLoading: isProductionLoading } = useQuery({ - queryKey: llmConnectionsQueryKeys.production(), - queryFn: getProductionConnection, + queryKey: llmConnectionsQueryKeys.production(productionFilters), + queryFn: () => getProductionConnection(productionFilters), }); @@ -50,11 +57,23 @@ const LLMConnections: FC = () => { setFilters(prev => ({ ...prev, pageNumber: pageIndex })); }, [pageIndex]); + // Sync production filters with main filters on component mount + useEffect(() => { + setProductionFilters(prev => ({ + ...prev, + llmPlatform: filters.llmPlatform || '', + llmModel: filters.llmModel || '', + sortBy: filters.sortBy || 'created_at', + sortOrder: filters.sortOrder || 'desc', + })); + }, [filters.llmPlatform, filters.llmModel, filters.sortBy, filters.sortOrder]); + const handleFilterChange = ( name: string, value: string | number | undefined | { name: string; id: string } ) => { let filterUpdate: Partial = {}; + let productionFilterUpdate: Partial = {}; if (name === 'sorting') { // Handle sorting format - no conversion needed, use snake_case directly @@ -62,11 +81,21 @@ const LLMConnections: FC = () => { const [sortBy, sortOrder] = sortingValue.split(' '); filterUpdate = { + sortBy: sortBy, + sortOrder: sortOrder as 'asc' | 'desc' + }; + + productionFilterUpdate = { sortBy: sortBy, sortOrder: sortOrder as 'asc' | 'desc' }; } else { filterUpdate = { [name]: value }; + + // Update production filters for relevant fields + if (name === 'llmPlatform' || name === 'llmModel') { + productionFilterUpdate = { [name]: value as string }; + } } setFilters((prevFilters) => ({ @@ -74,6 +103,14 @@ const LLMConnections: FC = () => { ...filterUpdate, })); + // Update production filters if relevant + if (Object.keys(productionFilterUpdate).length > 0) { + setProductionFilters((prevFilters) => ({ + ...prevFilters, + ...productionFilterUpdate, + })); + } + // Reset to first page when filters change if (name !== 'pageNumber') { setPageIndex(1); @@ -192,6 +229,12 @@ const LLMConnections: FC = () => { llmModel: '', environment: '', }); + setProductionFilters({ + sortBy: 'created_at', + sortOrder: 'desc', + llmPlatform: '', + llmModel: '', + }); setPageIndex(1); }} appearance={ButtonAppearanceTypes.SECONDARY} @@ -202,7 +245,7 @@ const LLMConnections: FC = () => {
- {productionConnection && ( + {productionConnection && filters?.environment !== "testing" && (

Production LLM Connection

diff --git a/GUI/src/pages/TestProductionLLM/index.tsx b/GUI/src/pages/TestProductionLLM/index.tsx index b5334c1..a9c1493 100644 --- a/GUI/src/pages/TestProductionLLM/index.tsx +++ b/GUI/src/pages/TestProductionLLM/index.tsx @@ -124,11 +124,11 @@ const TestProductionLLM: FC = () => { setMessages(prev => [...prev, botMessage]); // Show toast notification - toast.open({ - type: botMessageType, - title: t('errorOccurred'), - message: t('errorMessage'), - }); + // toast.open({ + // type: botMessageType, + // title: t('errorOccurred'), + // message: t('errorMessage'), + // }); } catch (error) { console.error('Error sending message:', error); diff --git a/GUI/src/services/llmConnections.ts b/GUI/src/services/llmConnections.ts index 83882ab..5b3921c 100644 --- a/GUI/src/services/llmConnections.ts +++ b/GUI/src/services/llmConnections.ts @@ -30,6 +30,13 @@ export interface LLMConnection { accessKey?: string; // Embedding model credentials embeddingModelApiKey?: string; + // Embedding AWS Bedrock credentials + embeddingAccessKey?: string; + embeddingSecretKey?: string; + // Embedding Azure credentials + embeddingDeploymentName?: string; + embeddingTargetUri?: string; + embeddingAzureApiKey?: string; } export interface LLMConnectionsResponse { @@ -69,6 +76,16 @@ export interface LLMConnectionFilters { environment?: string; status?: string; } + +export interface ProductionConnectionFilters { + llmPlatform?: string; + llmModel?: string; + embeddingPlatform?: string; + embeddingModel?: string; + connectionStatus?: string; + sortBy?: string; + sortOrder?: string; +} export interface LegacyLLMConnectionFilters { page: number; pageSize: number; @@ -98,11 +115,18 @@ export interface LLMConnectionFormData { accessKey?: string; // Embedding model credentials embeddingModelApiKey?: string; + // Embedding AWS Bedrock credentials + embeddingAccessKey?: string; + embeddingSecretKey?: string; + // Embedding Azure credentials + embeddingDeploymentName?: string; + embeddingTargetUri?: string; + embeddingAzureApiKey?: string; } // Vault secret service functions async function createVaultSecret(connectionId: string, connectionData: LLMConnectionFormData): Promise { - + const payload = { connectionId, llmPlatform: connectionData.llmPlatform, @@ -121,19 +145,29 @@ async function createVaultSecret(connectionId: string, connectionData: LLMConnec targetUrl: connectionData.targetUri || '', apiKey: connectionData.apiKey || '', }), - embeddingModelApiKey: connectionData.embeddingModelApiKey || '', + // Embedding AWS Bedrock credentials + ...(connectionData.embeddingModelPlatform === 'aws' && { + embeddingAccessKey: connectionData.embeddingAccessKey || '', + embeddingSecretKey: connectionData.embeddingSecretKey || '', + }), + // Embedding Azure credentials + ...(connectionData.embeddingModelPlatform === 'azure' && { + embeddingDeploymentName: connectionData.embeddingDeploymentName || '', + embeddingTargetUri: connectionData.embeddingTargetUri || '', + embeddingAzureApiKey: connectionData.embeddingAzureApiKey || '', + }), }; await apiDev.post(vaultEndpoints.CREATE_VAULT_SECRET(), payload); } async function deleteVaultSecret(connectionId: string, connectionData: Partial): Promise { - + const payload = { connectionId, llmPlatform: connectionData.llmPlatform || '', llmModel: connectionData.llmModel || '', - embeddingModel: connectionData.embeddingModel || '', + embeddingModel: connectionData.embeddingModel || '', embeddingPlatform: connectionData.embeddingModelPlatform || '', deploymentEnvironment: connectionData.deploymentEnvironment?.toLowerCase() || '', }; @@ -164,8 +198,22 @@ export async function getLLMConnection(id: string | number): Promise { - const { data } = await apiDev.get(llmConnectionsEndpoints.GET_PRODUCTION_CONNECTION()); +export async function getProductionConnection(filters?: ProductionConnectionFilters): Promise { + const queryParams = new URLSearchParams(); + + if (filters?.llmPlatform) queryParams.append('llmPlatform', filters.llmPlatform); + if (filters?.llmModel) queryParams.append('llmModel', filters.llmModel); + if (filters?.embeddingPlatform) queryParams.append('embeddingPlatform', filters.embeddingPlatform); + if (filters?.embeddingModel) queryParams.append('embeddingModel', filters.embeddingModel); + if (filters?.connectionStatus) queryParams.append('connectionStatus', filters.connectionStatus); + if (filters?.sortBy) queryParams.append('sortBy', filters.sortBy); + if (filters?.sortOrder) queryParams.append('sortOrder', filters.sortOrder); + + const url = queryParams.toString() + ? `${llmConnectionsEndpoints.GET_PRODUCTION_CONNECTION()}?${queryParams.toString()}` + : llmConnectionsEndpoints.GET_PRODUCTION_CONNECTION(); + + const { data } = await apiDev.get(url); return data?.response?.[0] || null; } @@ -190,11 +238,17 @@ export async function createLLMConnection(connectionData: LLMConnectionFormData) secret_key: maskSensitiveKey(connectionData.secretKey) || "", access_key: maskSensitiveKey(connectionData.accessKey) || "", // Embedding model credentials - embedding_model_api_key: maskSensitiveKey(connectionData.embeddingModelApiKey) || "", + // Embedding AWS Bedrock credentials + embedding_access_key: maskSensitiveKey(connectionData.embeddingAccessKey) || "", + embedding_secret_key: maskSensitiveKey(connectionData.embeddingSecretKey) || "", + // Embedding Azure credentials + embedding_deployment_name: connectionData.embeddingDeploymentName || "", + embedding_target_uri: connectionData.embeddingTargetUri || "", + embedding_azure_api_key: maskSensitiveKey(connectionData.embeddingAzureApiKey) || "", }); - + const connection = data?.response; - + // After successful database creation, store secrets in vault if (connection && connection.id) { try { @@ -205,7 +259,7 @@ export async function createLLMConnection(connectionData: LLMConnectionFormData) // The connection is already created in the database } } - + return connection; } @@ -233,22 +287,30 @@ export async function updateLLMConnection( secret_key: maskSensitiveKey(connectionData.secretKey) || "", access_key: maskSensitiveKey(connectionData.accessKey) || "", // Embedding model credentials - embedding_model_api_key: maskSensitiveKey(connectionData.embeddingModelApiKey) || "", + // Embedding AWS Bedrock credentials + embedding_access_key: maskSensitiveKey(connectionData.embeddingAccessKey) || "", + embedding_secret_key: maskSensitiveKey(connectionData.embeddingSecretKey) || "", + // Embedding Azure credentials + embedding_deployment_name: connectionData.embeddingDeploymentName || "", + embedding_target_uri: connectionData.embeddingTargetUri || "", + embedding_azure_api_key: maskSensitiveKey(connectionData.embeddingAzureApiKey) || "", }); - + const connection = data?.response; - - // After successful database update, update secrets in vault - if (connection) { + + if (connection && (connectionData.secretKey && !connectionData.secretKey?.includes('*') + || connectionData.accessKey && !connectionData.accessKey?.includes('*') + || connectionData.apiKey && !connectionData.apiKey?.includes('*') + || connectionData.embeddingAccessKey && !connectionData.embeddingAccessKey?.includes('*') + || connectionData.embeddingSecretKey && !connectionData.embeddingSecretKey?.includes('*') + || connectionData.embeddingAzureApiKey && !connectionData.embeddingAzureApiKey?.includes('*'))) { try { await createVaultSecret(id.toString(), connectionData); } catch (vaultError) { console.error('Failed to update secrets in vault:', vaultError); - // Note: We don't throw here to avoid breaking the connection update flow - // The connection is already updated in the database } } - + return connection; } @@ -260,12 +322,12 @@ export async function deleteLLMConnection(id: string | number): Promise { } catch (error) { console.error('Failed to get connection data before deletion:', error); } - + // Delete from database await apiDev.post(llmConnectionsEndpoints.DELETE_LLM_CONNECTION(), { connection_id: id, }); - + // After successful database deletion, delete secrets from vault if (connectionToDelete) { try { @@ -293,9 +355,9 @@ export async function checkBudgetStatus(): Promise { return null; } } - + export async function updateLLMConnectionStatus( - id: string | number, + id: string | number, status: 'active' | 'inactive' ): Promise { const { data } = await apiDev.post(llmConnectionsEndpoints.UPDATE_LLM_CONNECTION_STATUS(), { diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts index e004497..e10462e 100644 --- a/GUI/src/utils/queryKeys.ts +++ b/GUI/src/utils/queryKeys.ts @@ -1,5 +1,5 @@ import { PaginationState, SortingState } from '@tanstack/react-table'; -import { LLMConnectionFilters, LegacyLLMConnectionFilters } from 'services/llmConnections'; +import { LLMConnectionFilters, LegacyLLMConnectionFilters, ProductionConnectionFilters } from 'services/llmConnections'; import { InferenceRequest } from 'services/inference'; @@ -30,7 +30,7 @@ export const llmConnectionsQueryKeys = { details: () => [...llmConnectionsQueryKeys.all(), 'detail'] as const, detail: (id: string | number) => [...llmConnectionsQueryKeys.details(), id] as const, budgetStatus: () => [...llmConnectionsQueryKeys.all(), 'budget-status'] as const, - production: () => [...llmConnectionsQueryKeys.all(), 'production'] as const, + production: (filters?: ProductionConnectionFilters) => [...llmConnectionsQueryKeys.all(), 'production', filters] as const, }; export const inferenceQueryKeys = { diff --git a/endpoints.md b/endpoints.md index 6bd4fc9..262e81a 100644 --- a/endpoints.md +++ b/endpoints.md @@ -357,12 +357,41 @@ GET /ruuter-private/llm/connections/list | `llmPlatform` | `string` | Filter by LLM platform | | `llmModel` | `string` | Filter by LLM model | | `deploymentEnvironment` | `string` | Filter by environment (Testing / Production) | +| `pageNumber` | `number` | Page number (1-based) | +| `pageSize` | `number` | Number of items per page | +| `sortBy` | `string` | Field to sort by | +| `sortOrder` | `string` | Sort order: 'asc' or 'desc' | ### Example Request ```http GET /ruuter-private/llm/connections/list?llmPlatform=OpenAI&deploymentEnvironment=Testing&model=GPT4 ``` +--- + +## 5. Get Production LLM Connection (with filters) + +### Endpoint +```http +GET /ruuter-private/llm/connections/production +``` + +### Query Parameters (Optional for filtering) +| Parameter | Type | Description | +|-----------|------|-------------| +| `llmPlatform` | `string` | Filter by LLM platform | +| `llmModel` | `string` | Filter by LLM model | +| `embeddingPlatform` | `string` | Filter by embedding platform | +| `embeddingModel` | `string` | Filter by embedding model | +| `connectionStatus` | `string` | Filter by connection status | +| `sortBy` | `string` | Field to sort by | +| `sortOrder` | `string` | Sort order: 'asc' or 'desc' | + +### Example Request +```http +GET /ruuter-private/llm/connections/production?llmPlatform=OpenAI&connectionStatus=active +``` + ### Response (200 OK) ```json [ diff --git a/pyproject.toml b/pyproject.toml index 760dbb7..774f8af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "anthropic>=0.69.0", "nemoguardrails>=0.16.0", "tiktoken>=0.11.0", + "langfuse>=3.8.1", ] [tool.pyright] diff --git a/src/contextual_retrieval/contextual_retriever.py b/src/contextual_retrieval/contextual_retriever.py index e76165a..8ab5d24 100644 --- a/src/contextual_retrieval/contextual_retriever.py +++ b/src/contextual_retrieval/contextual_retriever.py @@ -14,7 +14,7 @@ from loguru import logger import asyncio import time - +from langfuse import observe from contextual_retrieval.config import ConfigLoader, ContextualRetrievalConfig # Type checking import to avoid circular dependency at runtime @@ -126,6 +126,7 @@ def _clear_session_cache(self): logger.debug("Clearing session LLM service cache") self._session_llm_service = None + @observe(name="retrieve_contextual_chunks", as_type="retriever") async def retrieve_contextual_chunks( self, original_question: str, diff --git a/src/llm_orchestration_service.py b/src/llm_orchestration_service.py index 08f3596..b5d5f7d 100644 --- a/src/llm_orchestration_service.py +++ b/src/llm_orchestration_service.py @@ -5,6 +5,7 @@ import asyncio import os from loguru import logger +from langfuse import Langfuse, observe from llm_orchestrator_config.llm_manager import LLMManager from models.request_models import ( @@ -28,6 +29,36 @@ from src.contextual_retrieval import ContextualRetriever +class LangfuseConfig: + """Configuration for Langfuse integration.""" + + def __init__(self): + self.langfuse_client: Optional[Langfuse] = None + self._initialize_langfuse() + + def _initialize_langfuse(self): + """Initialize Langfuse client with Vault secrets.""" + try: + from llm_orchestrator_config.vault.vault_client import VaultAgentClient + + vault = VaultAgentClient() + if vault.is_vault_available(): + langfuse_secrets = vault.get_secret("langfuse/config") + if langfuse_secrets: + self.langfuse_client = Langfuse( + public_key=langfuse_secrets.get("public_key"), + secret_key=langfuse_secrets.get("secret_key"), + host=langfuse_secrets.get("host", "http://langfuse-web:3000"), + ) + logger.info("Langfuse client initialized successfully") + else: + logger.warning("Langfuse secrets not found in Vault") + else: + logger.warning("Vault not available, Langfuse tracing disabled") + except Exception as e: + logger.warning(f"Failed to initialize Langfuse: {e}") + + class LLMOrchestrationService: """ Service class for handling LLM orchestration with integrated guardrails. @@ -39,8 +70,9 @@ class LLMOrchestrationService: def __init__(self) -> None: """Initialize the orchestration service.""" - pass + self.langfuse_config = LangfuseConfig() + @observe(name="orchestration_request", as_type="agent") def process_orchestration_request( self, request: OrchestrationRequest ) -> Union[OrchestrationResponse, TestOrchestrationResponse]: @@ -82,6 +114,38 @@ def process_orchestration_request( # Log final costs and return response self._log_costs(costs_dict) + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + total_costs = calculate_total_costs(costs_dict) + + total_input_tokens = sum( + c.get("total_prompt_tokens", 0) for c in costs_dict.values() + ) + total_output_tokens = sum( + c.get("total_completion_tokens", 0) for c in costs_dict.values() + ) + + langfuse.update_current_generation( + model=components["llm_manager"] + .get_provider_info() + .get("model", "unknown"), + usage_details={ + "input": total_input_tokens, + "output": total_output_tokens, + "total": total_costs.get("total_tokens", 0), + }, + cost_details={ + "total": total_costs.get("total_cost", 0.0), + }, + metadata={ + "total_calls": total_costs.get("total_calls", 0), + "cost_breakdown": costs_dict, + "chat_id": request.chatId, + "author_id": request.authorId, + "environment": request.environment, + }, + ) + langfuse.flush() return response except Exception as e: @@ -89,9 +153,20 @@ def process_orchestration_request( f"Error processing orchestration request for chatId: {request.chatId}, " f"error: {str(e)}" ) + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + metadata={ + "error": str(e), + "error_type": type(e).__name__, + "response_type": "technical_issue", + } + ) + langfuse.flush() self._log_costs(costs_dict) return self._create_error_response(request) + @observe(name="initialize_service_components", as_type="span") def _initialize_service_components( self, request: OrchestrationRequest ) -> Dict[str, Any]: @@ -212,6 +287,7 @@ def _log_generator_status(self, components: Dict[str, Any]) -> None: except Exception as e: logger.warning(f" Generator: Status check failed - {str(e)}") + @observe(name="execute_orchestration_pipeline", as_type="span") def _execute_orchestration_pipeline( self, request: OrchestrationRequest, @@ -262,6 +338,7 @@ def _execute_orchestration_pipeline( components["guardrails_adapter"], generated_response, request, costs_dict ) + @observe(name="safe_initialize_guardrails", as_type="span") def _safe_initialize_guardrails( self, environment: str, connection_id: Optional[str] ) -> Optional[NeMoRailsAdapter]: @@ -275,6 +352,7 @@ def _safe_initialize_guardrails( logger.warning("Continuing without guardrails protection") return None + @observe(name="safe_initialize_contextual_retriever", as_type="span") def _safe_initialize_contextual_retriever( self, environment: str, connection_id: Optional[str] ) -> Optional[ContextualRetriever]: @@ -292,6 +370,7 @@ def _safe_initialize_contextual_retriever( logger.warning("Continuing without chunk retrieval capabilities") return None + @observe(name="safe_initialize_response_generator", as_type="span") def _safe_initialize_response_generator( self, llm_manager: LLMManager ) -> Optional[ResponseGeneratorAgent]: @@ -449,6 +528,7 @@ def _create_out_of_scope_response( content=OUT_OF_SCOPE_MESSAGE, ) + @observe(name="initialize_guardrails", as_type="span") def _initialize_guardrails( self, environment: str, connection_id: Optional[str] ) -> NeMoRailsAdapter: @@ -479,6 +559,7 @@ def _initialize_guardrails( logger.error(f"Failed to initialize Guardrails adapter: {str(e)}") raise + @observe(name="check_input_guardrails", as_type="span") def _check_input_guardrails( self, guardrails_adapter: NeMoRailsAdapter, @@ -503,7 +584,26 @@ def _check_input_guardrails( # Store guardrail costs costs_dict["input_guardrails"] = result.usage - + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + input=user_message, + metadata={ + "guardrail_type": "input", + "allowed": result.allowed, + "verdict": result.verdict, + "blocked_reason": result.reason if not result.allowed else None, + "error": result.error if result.error else None, + }, + usage_details={ + "input": result.usage.get("total_prompt_tokens", 0), + "output": result.usage.get("total_completion_tokens", 0), + "total": result.usage.get("total_tokens", 0), + }, # type: ignore + cost_details={ + "total": result.usage.get("total_cost", 0.0), + }, + ) logger.info( f"Input guardrails check completed: allowed={result.allowed}, " f"cost=${result.usage.get('total_cost', 0):.6f}" @@ -513,6 +613,15 @@ def _check_input_guardrails( except Exception as e: logger.error(f"Input guardrails check failed: {str(e)}") + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + metadata={ + "error": str(e), + "error_type": type(e).__name__, + "guardrail_type": "input", + } + ) # Return conservative result on error return GuardrailCheckResult( allowed=False, @@ -522,6 +631,7 @@ def _check_input_guardrails( usage={}, ) + @observe(name="check_output_guardrails", as_type="span") def _check_output_guardrails( self, guardrails_adapter: NeMoRailsAdapter, @@ -546,7 +656,28 @@ def _check_output_guardrails( # Store guardrail costs costs_dict["output_guardrails"] = result.usage - + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + input=assistant_message[:500], # Truncate for readability + output=result.verdict, + metadata={ + "guardrail_type": "output", + "allowed": result.allowed, + "verdict": result.verdict, + "reason": result.reason if not result.allowed else None, + "error": result.error if result.error else None, + "response_length": len(assistant_message), + }, + usage_details={ + "input": result.usage.get("total_prompt_tokens", 0), + "output": result.usage.get("total_completion_tokens", 0), + "total": result.usage.get("total_tokens", 0), + }, # type: ignore + cost_details={ + "total": result.usage.get("total_cost", 0.0), + }, + ) logger.info( f"Output guardrails check completed: allowed={result.allowed}, " f"cost=${result.usage.get('total_cost', 0):.6f}" @@ -556,6 +687,15 @@ def _check_output_guardrails( except Exception as e: logger.error(f"Output guardrails check failed: {str(e)}") + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + metadata={ + "error": str(e), + "error_type": type(e).__name__, + "guardrail_type": "output", + } + ) # Return conservative result on error return GuardrailCheckResult( allowed=False, @@ -631,6 +771,7 @@ def _log_costs(self, costs_dict: Dict[str, Dict[str, Any]]) -> None: except Exception as e: logger.warning(f"Failed to log costs: {str(e)}") + @observe(name="initialize_llm_manager", as_type="span") def _initialize_llm_manager( self, environment: str, connection_id: Optional[str] ) -> LLMManager: @@ -660,6 +801,7 @@ def _initialize_llm_manager( logger.error(f"Failed to initialize LLM Manager: {str(e)}") raise + @observe(name="refine_user_prompt", as_type="chain") def _refine_user_prompt( self, llm_manager: LLMManager, @@ -725,7 +867,32 @@ def _refine_user_prompt( raise ValueError( f"Prompt refinement validation failed: {str(validation_error)}" ) from validation_error - + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + refinement_applied = ( + original_message.strip() + != validated_output.original_question.strip() + ) + langfuse.update_current_generation( + model=llm_manager.get_provider_info().get("model", "unknown"), + input=original_message, + usage_details={ + "input": usage_info.get("total_prompt_tokens", 0), + "output": usage_info.get("total_completion_tokens", 0), + "total": usage_info.get("total_tokens", 0), + }, + cost_details={ + "total": usage_info.get("total_cost", 0.0), + }, + metadata={ + "num_calls": usage_info.get("num_calls", 0), + "num_refined_questions": len( + validated_output.refined_questions + ), + "refinement_applied": refinement_applied, + "conversation_history_length": len(history), + }, # type: ignore + ) output_json = validated_output.model_dump() logger.info( f"Prompt refinement output: {json.dumps(output_json, indent=2)}" @@ -738,9 +905,19 @@ def _refine_user_prompt( raise except Exception as e: logger.error(f"Prompt refinement failed: {str(e)}") + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + metadata={ + "error": str(e), + "error_type": type(e).__name__, + "refinement_failed": True, + } + ) logger.error(f"Failed to refine message: {original_message}") raise RuntimeError(f"Prompt refinement process failed: {str(e)}") from e + @observe(name="initialize_contextual_retriever", as_type="span") def _initialize_contextual_retriever( self, environment: str, connection_id: Optional[str] ) -> ContextualRetriever: @@ -774,6 +951,7 @@ def _initialize_contextual_retriever( logger.error(f"Failed to initialize contextual retriever: {str(e)}") raise + @observe(name="initialize_response_generator", as_type="span") def _initialize_response_generator( self, llm_manager: LLMManager ) -> ResponseGeneratorAgent: @@ -800,6 +978,7 @@ def _initialize_response_generator( logger.error(f"Failed to initialize response generator: {str(e)}") raise + @observe(name="generate_rag_response", as_type="generation") def _generate_rag_response( self, llm_manager: LLMManager, @@ -867,7 +1046,27 @@ def _generate_rag_response( }, ) costs_dict["response_generator"] = generator_usage - + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + model=llm_manager.get_provider_info().get("model", "unknown"), + usage_details={ + "input": generator_usage.get("total_prompt_tokens", 0), + "output": generator_usage.get("total_completion_tokens", 0), + "total": generator_usage.get("total_tokens", 0), + }, + cost_details={ + "total": generator_usage.get("total_cost", 0.0), + }, + metadata={ + "num_calls": generator_usage.get("num_calls", 0), + "question_out_of_scope": question_out_of_scope, + "num_chunks_used": len(relevant_chunks) + if relevant_chunks + else 0, + }, + output=answer, + ) if question_out_of_scope: logger.info("Question determined out-of-scope – sending fixed message.") if request.environment == "test": @@ -910,6 +1109,16 @@ def _generate_rag_response( except Exception as e: logger.error(f"RAG Response generation failed: {str(e)}") + if self.langfuse_config.langfuse_client: + langfuse = self.langfuse_config.langfuse_client + langfuse.update_current_generation( + metadata={ + "error": str(e), + "error_type": type(e).__name__, + "response_type": "technical_issue", + "refinement_failed": False, + } + ) # Standardized technical issue; no second LLM call, no citations if request.environment == "test": logger.info( @@ -933,7 +1142,7 @@ def _generate_rag_response( # ======================================================================== # Vector Indexer Support Methods (Isolated from RAG Pipeline) # ======================================================================== - + @observe(name="create_embeddings_for_indexer", as_type="span") def create_embeddings_for_indexer( self, texts: List[str], diff --git a/src/llm_orchestrator_config/context_manager.py b/src/llm_orchestrator_config/context_manager.py index d1e0358..a14447e 100644 --- a/src/llm_orchestrator_config/context_manager.py +++ b/src/llm_orchestrator_config/context_manager.py @@ -6,6 +6,7 @@ from src.llm_orchestrator_config.llm_manager import LLMManager from src.models.request_models import ContextGenerationRequest +from langfuse import observe class ContextGenerationManager: @@ -30,6 +31,7 @@ def __init__(self, llm_manager: LLMManager) -> None: # Cache structure prepared for future prompt caching implementation self._cache: Dict[str, Any] = {} + @observe(name="generate_context_with_caching", as_type="generation") def generate_context_with_caching( self, request: ContextGenerationRequest ) -> Dict[str, Any]: diff --git a/uv.lock b/uv.lock index 8653912..5f79bf1 100644 --- a/uv.lock +++ b/uv.lock @@ -1104,6 +1104,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/0d/41a51b40d24ff0384ec4f7ab8dd3dcea8353c05c973836b5e289f1465d4f/langchain_text_splitters-0.3.11-py3-none-any.whl", hash = "sha256:cf079131166a487f1372c8ab5d0bfaa6c0a4291733d9c43a34a16ac9bcd6a393", size = 33845, upload-time = "2025-08-31T23:02:57.195Z" }, ] +[[package]] +name = "langfuse" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "httpx" }, + { name = "openai" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/0b/81f9c6a982f79c112b7f10bfd6f3a4871e6fa3e4fe8d078b6112abfd3c08/langfuse-3.8.1.tar.gz", hash = "sha256:2464ae3f8386d80e1252a0e7406e3be4121e792a74f1b1c21d9950f658e5168d", size = 197401, upload-time = "2025-10-22T13:35:52.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/f9/538af0fc4219eb2484ba319483bce3383146f7a0923d5f39e464ad9a504b/langfuse-3.8.1-py3-none-any.whl", hash = "sha256:5b94b66ec0b0de388a8ea1f078b32c1666b5825b36eab863a21fdee78c53b3bb", size = 364580, upload-time = "2025-10-22T13:35:50.597Z" }, +] + [[package]] name = "langsmith" version = "0.4.37" @@ -1499,6 +1520,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.38.0" @@ -2083,6 +2122,7 @@ dependencies = [ { name = "dspy" }, { name = "fastapi" }, { name = "hvac" }, + { name = "langfuse" }, { name = "loguru" }, { name = "nemoguardrails" }, { name = "numpy" }, @@ -2114,6 +2154,7 @@ requires-dist = [ { name = "dspy", specifier = ">=3.0.3" }, { name = "fastapi", specifier = ">=0.116.1" }, { name = "hvac", specifier = ">=2.3.0" }, + { name = "langfuse", specifier = ">=3.8.1" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "nemoguardrails", specifier = ">=0.16.0" }, { name = "numpy", specifier = ">=2.3.2" }, @@ -2663,23 +2704,21 @@ wheels = [ [[package]] name = "wrapt" -version = "2.0.0" +version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/19/5e5bcd855d808892fe02d49219f97a50f64cd6d8313d75df3494ee97b1a3/wrapt-2.0.0.tar.gz", hash = "sha256:35a542cc7a962331d0279735c30995b024e852cf40481e384fd63caaa391cbb9", size = 81722, upload-time = "2025-10-19T23:47:54.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/28/7f266b5bf50c3ad0c99c524d99faa0f7d6eecb045d950e7d2c9e1f0e1338/wrapt-2.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73c6f734aecb1a030d9a265c13a425897e1ea821b73249bb14471445467ca71c", size = 78078, upload-time = "2025-10-19T23:45:58.855Z" }, - { url = "https://files.pythonhosted.org/packages/06/0c/bbdcad7eb535fae9d6b0fcfa3995c364797cd8e2b423bba5559ab2d88dcf/wrapt-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4a7f8023b8ce8a36370154733c747f8d65c8697cb977d8b6efeb89291fff23e", size = 61158, upload-time = "2025-10-19T23:46:00.096Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/bba3e7a4ebf4d1624103ee59d97b78a1fbb08fb5753ff5d1b69f5ef5e863/wrapt-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cb62f686c50e9dab5983c68f6c8e9cbf14a6007935e683662898a7d892fa69", size = 61646, upload-time = "2025-10-19T23:46:01.279Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0c/0f565294897a72493dbafe7b46229b5f09f3776795a894d6b737e98387de/wrapt-2.0.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:43dc0550ae15e33e6bb45a82a5e1b5495be2587fbaa996244b509921810ee49f", size = 121442, upload-time = "2025-10-19T23:46:04.287Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/7f03501a8a078ad79b19b1a888f9192a9494e62ddf8985267902766a4f30/wrapt-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39c5b45b056d630545e40674d1f5e1b51864b3546f25ab6a4a331943de96262e", size = 123018, upload-time = "2025-10-19T23:46:06.052Z" }, - { url = "https://files.pythonhosted.org/packages/37/6b/ad0e1ff98359f13b4b0c2c52848e792841146fe79ac5f56899b9a028fc0d/wrapt-2.0.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:804e88f824b76240a1b670330637ccfd2d18b9efa3bb4f02eb20b2f64880b324", size = 117369, upload-time = "2025-10-19T23:46:02.53Z" }, - { url = "https://files.pythonhosted.org/packages/ac/6c/a90437bba8cb1ce2ed639af979515e09784678c2a7f4ffc79f2cf7de809e/wrapt-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c2c476aa3fc2b9899c3f7b20963fac4f952e7edb74a31fc92f7745389a2e3618", size = 121453, upload-time = "2025-10-19T23:46:07.747Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a9/b3982f9bd15bd45857a23c48b7c36e47d05db4a4dcc5061c31f169238845/wrapt-2.0.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8d851e526891216f89fcb7a1820dad9bd503ba3468fb9635ee28e93c781aa98e", size = 116250, upload-time = "2025-10-19T23:46:09.385Z" }, - { url = "https://files.pythonhosted.org/packages/73/e2/b7a8b1afac9f791d8f5eac0d9726559f1d7ec4a2b5a6b4e67ac145b007a5/wrapt-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b95733c2360c4a8656ee93c7af78e84c0bd617da04a236d7a456c8faa34e7a2d", size = 120575, upload-time = "2025-10-19T23:46:11.882Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/37920eeea96094f450ae35505d39f1135df951a2cdee0d4e01d4f843396a/wrapt-2.0.0-cp312-cp312-win32.whl", hash = "sha256:ea56817176834edf143df1109ae8fdaa087be82fdad3492648de0baa8ae82bf2", size = 58175, upload-time = "2025-10-19T23:46:15.678Z" }, - { url = "https://files.pythonhosted.org/packages/f0/db/b395f3b0c7f2c60d9219afacc54ceb699801ccf2d3d969ba556dc6d3af20/wrapt-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c7d3bee7be7a2665286103f4d1f15405c8074e6e1f89dac5774f9357c9a3809", size = 60415, upload-time = "2025-10-19T23:46:12.913Z" }, - { url = "https://files.pythonhosted.org/packages/86/22/33d660214548af47fc59d9eec8c0e0693bcedc5b3a0b52e8cbdd61f3b646/wrapt-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:680f707e1d26acbc60926659799b15659f077df5897a6791c7c598a5d4a211c4", size = 58911, upload-time = "2025-10-19T23:46:13.889Z" }, - { url = "https://files.pythonhosted.org/packages/00/5c/c34575f96a0a038579683c7f10fca943c15c7946037d1d254ab9db1536ec/wrapt-2.0.0-py3-none-any.whl", hash = "sha256:02482fb0df89857e35427dfb844319417e14fae05878f295ee43fa3bf3b15502", size = 43998, upload-time = "2025-10-19T23:47:52.858Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]] diff --git a/vault/agent-out/pidfile b/vault/agent-out/pidfile deleted file mode 100644 index e69de29..0000000