diff --git a/cloud/api/api.ts b/cloud/api/api.ts index 7b18e005d7..1fe1c43e15 100644 --- a/cloud/api/api.ts +++ b/cloud/api/api.ts @@ -19,6 +19,7 @@ export * from "@/api/environments.schemas"; export * from "@/api/api-keys.schemas"; export * from "@/api/functions.schemas"; export * from "@/api/annotations.schemas"; +export * from "@/api/search.schemas"; export class MirascopeCloudApi extends HttpApi.make("MirascopeCloudApi") .add(HealthApi) diff --git a/cloud/api/handler.test.ts b/cloud/api/handler.test.ts index c238896be0..b09b792685 100644 --- a/cloud/api/handler.test.ts +++ b/cloud/api/handler.test.ts @@ -1,8 +1,13 @@ -import { Effect } from "effect"; +import { Effect, Layer } from "effect"; import { describe, it, expect } from "@/tests/api"; import { handleRequest } from "@/api/handler"; import type { PublicUser } from "@/db/schema"; import { HandlerError } from "@/errors"; +import { ClickHouse } from "@/clickhouse/client"; +import { ClickHouseSearch } from "@/clickhouse/search"; +import { SettingsService, getSettings } from "@/settings"; +import { CLICKHOUSE_CONNECTION_FILE } from "@/tests/global-setup"; +import fs from "fs"; const mockUser: PublicUser = { id: "test-user-id", @@ -11,9 +16,44 @@ const mockUser: PublicUser = { deletedAt: null, }; +type ClickHouseConnectionFile = { + url: string; + user: string; + password: string; + database: string; + nativePort: number; +}; + +function getTestClickHouseConfig(): ClickHouseConnectionFile { + try { + const raw = fs.readFileSync(CLICKHOUSE_CONNECTION_FILE, "utf-8"); + return JSON.parse(raw) as ClickHouseConnectionFile; + } catch { + throw new Error( + "TEST_CLICKHOUSE_URL not set. Ensure global-setup.ts ran successfully.", + ); + } +} + +const clickhouseConfig = getTestClickHouseConfig(); +const settings = getSettings(); +const settingsLayer = Layer.succeed(SettingsService, { + ...settings, + env: "test", + CLICKHOUSE_URL: clickhouseConfig.url, + CLICKHOUSE_USER: clickhouseConfig.user, + CLICKHOUSE_PASSWORD: clickhouseConfig.password, + CLICKHOUSE_DATABASE: clickhouseConfig.database, +}); +const clickHouseSearchLayer = ClickHouseSearch.Default.pipe( + Layer.provide(ClickHouse.Default), + Layer.provide(settingsLayer), +); + describe("handleRequest", () => { it.effect("should return 404 for non-existing routes", () => Effect.gen(function* () { + const clickHouseSearch = yield* ClickHouseSearch; const req = new Request( "http://localhost/api/v0/this-route-does-not-exist", { method: "GET" }, @@ -23,35 +63,39 @@ describe("handleRequest", () => { user: mockUser, environment: "test", prefix: "/api/v0", + clickHouseSearch, }); expect(response.status).toBe(404); expect(matched).toBe(false); - }), + }).pipe(Effect.provide(clickHouseSearchLayer)), ); it.effect( "should return 404 when pathname exactly matches prefix (no route)", () => Effect.gen(function* () { + const clickHouseSearch = yield* ClickHouseSearch; const req = new Request("http://localhost/api/v0", { method: "GET" }); const { matched, response } = yield* handleRequest(req, { user: mockUser, environment: "test", prefix: "/api/v0", + clickHouseSearch, }); // The path becomes "/" after stripping prefix, which doesn't match any route expect(response.status).toBe(404); expect(matched).toBe(false); - }), + }).pipe(Effect.provide(clickHouseSearchLayer)), ); it.effect( "should return error for a request that triggers an exception", () => Effect.gen(function* () { + const clickHouseSearch = yield* ClickHouseSearch; const faultyRequest = new Proxy( {}, { @@ -64,17 +108,19 @@ describe("handleRequest", () => { const error = yield* handleRequest(faultyRequest, { user: mockUser, environment: "test", + clickHouseSearch, }).pipe(Effect.flip); expect(error).toBeInstanceOf(HandlerError); expect(error.message).toContain( "[Effect API] Error handling request: boom", ); - }), + }).pipe(Effect.provide(clickHouseSearchLayer)), ); it.effect("should handle POST requests with body", () => Effect.gen(function* () { + const clickHouseSearch = yield* ClickHouseSearch; // POST request with body to trigger duplex: "half" const req = new Request( "http://localhost/api/v0/organizations/00000000-0000-0000-0000-000000000099/projects", @@ -89,15 +135,17 @@ describe("handleRequest", () => { user: mockUser, environment: "test", prefix: "/api/v0", + clickHouseSearch, }); expect(matched).toBe(true); expect(response.status).toBeGreaterThanOrEqual(400); - }), + }).pipe(Effect.provide(clickHouseSearchLayer)), ); it.effect("should transform _tag in JSON error responses", () => Effect.gen(function* () { + const clickHouseSearch = yield* ClickHouseSearch; // Trigger a NotFoundError by trying to get a non-existent organization const req = new Request( "http://localhost/api/v0/organizations/00000000-0000-0000-0000-000000000099", @@ -110,6 +158,7 @@ describe("handleRequest", () => { user: mockUser, environment: "test", prefix: "/api/v0", + clickHouseSearch, }); const body = yield* Effect.promise(() => response.text()); @@ -119,6 +168,6 @@ describe("handleRequest", () => { // Ensure _tag is transformed to tag in error responses expect(body).toContain('"tag"'); expect(body).not.toContain('"_tag"'); - }), + }).pipe(Effect.provide(clickHouseSearchLayer)), ); }); diff --git a/cloud/api/handler.ts b/cloud/api/handler.ts index 36ffba03fe..8e1a820983 100644 --- a/cloud/api/handler.ts +++ b/cloud/api/handler.ts @@ -2,9 +2,10 @@ import { HttpApiBuilder, HttpServer } from "@effect/platform"; import { Context, Effect, Layer } from "effect"; import { ApiLive } from "@/api/router"; import { HandlerError } from "@/errors"; -import { SettingsService } from "@/settings"; +import { SettingsService, getSettings } from "@/settings"; import { Database } from "@/db"; import { Payments } from "@/payments"; +import { ClickHouseSearch } from "@/clickhouse/search"; import { AuthenticatedUser, Authentication } from "@/auth"; import type { PublicUser, ApiKeyInfo } from "@/db/schema"; @@ -13,11 +14,13 @@ export type HandleRequestOptions = { user: PublicUser; apiKeyInfo?: ApiKeyInfo; environment: string; + clickHouseSearch: Context.Tag.Service; }; type WebHandlerOptions = { db: Context.Tag.Service; payments: Context.Tag.Service; + clickHouseSearch: Context.Tag.Service; user: PublicUser; apiKeyInfo?: ApiKeyInfo; environment: string; @@ -25,7 +28,10 @@ type WebHandlerOptions = { function createWebHandler(options: WebHandlerOptions) { const services = Layer.mergeAll( - Layer.succeed(SettingsService, { env: options.environment }), + Layer.succeed(SettingsService, { + ...getSettings(), + env: options.environment, + }), Layer.succeed(AuthenticatedUser, options.user), Layer.succeed(Authentication, { user: options.user, @@ -33,6 +39,7 @@ function createWebHandler(options: WebHandlerOptions) { }), Layer.succeed(Database, options.db), Layer.succeed(Payments, options.payments), + Layer.succeed(ClickHouseSearch, options.clickHouseSearch), ); const ApiWithDependencies = Layer.merge( @@ -71,6 +78,7 @@ export const handleRequest = ( user: options.user, apiKeyInfo: options.apiKeyInfo, environment: options.environment, + clickHouseSearch: options.clickHouseSearch, }); const result = yield* Effect.tryPromise({ diff --git a/cloud/api/router.ts b/cloud/api/router.ts index 3e841b474a..917afae7e4 100644 --- a/cloud/api/router.ts +++ b/cloud/api/router.ts @@ -46,6 +46,11 @@ import { updateAnnotationHandler, deleteAnnotationHandler, } from "@/api/annotations.handlers"; +import { + searchHandler, + getTraceDetailHandler, + getAnalyticsSummaryHandler, +} from "@/api/search.handlers"; import { MirascopeCloudApi } from "@/api/api"; export { MirascopeCloudApi }; @@ -60,7 +65,16 @@ const TracesHandlersLive = HttpApiBuilder.group( MirascopeCloudApi, "traces", (handlers) => - handlers.handle("create", ({ payload }) => createTraceHandler(payload)), + handlers + // TODO: Add missing router handlers e.g. list, listByFunctionHash, etc. + .handle("create", ({ payload }) => createTraceHandler(payload)) + .handle("search", ({ payload }) => searchHandler(payload)) + .handle("getTraceDetail", ({ path }) => + getTraceDetailHandler(path.traceId), + ) + .handle("getAnalyticsSummary", ({ urlParams }) => + getAnalyticsSummaryHandler(urlParams), + ), ); const DocsHandlersLive = HttpApiBuilder.group( diff --git a/cloud/api/search.handlers.ts b/cloud/api/search.handlers.ts new file mode 100644 index 0000000000..0d25b0a61c --- /dev/null +++ b/cloud/api/search.handlers.ts @@ -0,0 +1,129 @@ +/** + * @fileoverview Search API handlers for ClickHouse-powered search. + * + * Provides handlers for searching spans, retrieving trace details, + * and getting analytics summaries from ClickHouse. + * + * ## Authentication + * + * All endpoints require API key authentication. The environment ID + * is extracted from the API key scope. + * + * @example + * ```ts + * // Handler usage (internal) + * const result = yield* searchHandler({ startTime: "...", endTime: "..." }); + * ``` + */ + +import { Effect } from "effect"; +import { Authentication } from "@/auth"; +import { + ClickHouseSearch, + type SpanSearchInput, + type TraceDetailInput, + type AnalyticsSummaryInput, + type AttributeFilter, +} from "@/clickhouse/search"; +import type { + SearchRequest, + AnalyticsSummaryRequest, +} from "@/api/search.schemas"; + +export * from "@/api/search.schemas"; + +// ============================================================================= +// Handlers +// ============================================================================= + +/** + * Search spans with filters and pagination. + * + * Requires API key authentication. Uses the environment ID from the API key scope. + */ +export const searchHandler = (payload: SearchRequest) => + Effect.gen(function* () { + const { apiKeyInfo } = yield* Authentication.ApiKey; + const searchService = yield* ClickHouseSearch; + + // Convert readonly arrays to mutable arrays + const model = + payload.model && payload.model.length > 0 + ? [...payload.model] + : undefined; + const provider = + payload.provider && payload.provider.length > 0 + ? [...payload.provider] + : undefined; + const attributeFilters: AttributeFilter[] | undefined = + payload.attributeFilters + ? payload.attributeFilters.map((f) => ({ + key: f.key, + operator: f.operator, + value: f.value, + })) + : undefined; + + const input: SpanSearchInput = { + environmentId: apiKeyInfo.environmentId, + startTime: new Date(payload.startTime), + endTime: new Date(payload.endTime), + query: payload.query, + traceId: payload.traceId, + spanId: payload.spanId, + model, + provider, + functionId: payload.functionId, + functionName: payload.functionName, + hasError: payload.hasError, + minTokens: payload.minTokens, + maxTokens: payload.maxTokens, + minDuration: payload.minDuration, + maxDuration: payload.maxDuration, + attributeFilters, + limit: payload.limit, + offset: payload.offset, + sortBy: payload.sortBy, + sortOrder: payload.sortOrder, + }; + + return yield* searchService.search(input); + }); + +/** + * Get full trace detail with all spans. + * + * Requires API key authentication. Uses the environment ID from the API key scope. + */ +export const getTraceDetailHandler = (traceId: string) => + Effect.gen(function* () { + const { apiKeyInfo } = yield* Authentication.ApiKey; + const searchService = yield* ClickHouseSearch; + + const input: TraceDetailInput = { + environmentId: apiKeyInfo.environmentId, + traceId, + }; + + return yield* searchService.getTraceDetail(input); + }); + +/** + * Get analytics summary for a time range. + * + * Requires API key authentication. Uses the environment ID from the API key scope. + */ +export const getAnalyticsSummaryHandler = (params: AnalyticsSummaryRequest) => + Effect.gen(function* () { + const { apiKeyInfo } = yield* Authentication.ApiKey; + const searchService = yield* ClickHouseSearch; + + const input: AnalyticsSummaryInput = { + environmentId: apiKeyInfo.environmentId, + startTime: new Date(params.startTime), + endTime: new Date(params.endTime), + functionId: params.functionId, + }; + + return yield* searchService.getAnalyticsSummary(input); + }); diff --git a/cloud/api/search.schemas.ts b/cloud/api/search.schemas.ts new file mode 100644 index 0000000000..5c1219d4a2 --- /dev/null +++ b/cloud/api/search.schemas.ts @@ -0,0 +1,181 @@ +/** + * @fileoverview Search API schema definitions for ClickHouse-powered search. + * + * Provides endpoints for searching spans in ClickHouse analytics database, + * retrieving trace details, and getting analytics summaries. + * + * ## Endpoints (Traces API) + * + * - `POST /traces/search` - Search spans with filters and pagination + * - `GET /traces/:traceId` - Get full trace detail with all spans + * - `GET /traces/analytics` - Get analytics summary for a time range + * + * ## Query Constraints + * + * - `limit`: max 1000 (default 50) + * - `offset`: max 10000 + * - `time_range`: max 30 days (search), max 90 days (analytics) + * - `query`: max 500 characters (token-based matching, not substring) + * - Required: startTime + endTime + */ + +import { Schema } from "effect"; + +// ============================================================================= +// Attribute Filter Schema +// ============================================================================= + +const AttributeFilterOperatorSchema = Schema.Literal( + "eq", + "neq", + "contains", + "exists", +); + +const AttributeFilterSchema = Schema.Struct({ + key: Schema.String, + operator: AttributeFilterOperatorSchema, + value: Schema.optional(Schema.String), +}); + +// ============================================================================= +// Search Request/Response Schemas +// ============================================================================= + +export const SearchRequestSchema = Schema.Struct({ + startTime: Schema.String, + endTime: Schema.String, + query: Schema.optional(Schema.String), + traceId: Schema.optional(Schema.String), + spanId: Schema.optional(Schema.String), + model: Schema.optional(Schema.Array(Schema.String)), + provider: Schema.optional(Schema.Array(Schema.String)), + functionId: Schema.optional(Schema.String), + functionName: Schema.optional(Schema.String), + hasError: Schema.optional(Schema.Boolean), + minTokens: Schema.optional(Schema.Number), + maxTokens: Schema.optional(Schema.Number), + minDuration: Schema.optional(Schema.Number), + maxDuration: Schema.optional(Schema.Number), + attributeFilters: Schema.optional(Schema.Array(AttributeFilterSchema)), + limit: Schema.optional(Schema.Number), + offset: Schema.optional(Schema.Number), + sortBy: Schema.optional( + Schema.Literal("start_time", "duration_ms", "total_tokens"), + ), + sortOrder: Schema.optional(Schema.Literal("asc", "desc")), +}); + +export type SearchRequest = typeof SearchRequestSchema.Type; + +const SpanSearchResultSchema = Schema.Struct({ + id: Schema.String, + traceId: Schema.String, + spanId: Schema.String, + name: Schema.String, + startTime: Schema.String, + durationMs: Schema.NullOr(Schema.Number), + model: Schema.NullOr(Schema.String), + provider: Schema.NullOr(Schema.String), + totalTokens: Schema.NullOr(Schema.Number), + functionId: Schema.NullOr(Schema.String), + functionName: Schema.NullOr(Schema.String), +}); + +export type SpanSearchResult = typeof SpanSearchResultSchema.Type; + +export const SearchResponseSchema = Schema.Struct({ + spans: Schema.Array(SpanSearchResultSchema), + total: Schema.Number, + hasMore: Schema.Boolean, +}); + +export type SearchResponse = typeof SearchResponseSchema.Type; + +// ============================================================================= +// Trace Detail Schemas +// ============================================================================= + +const SpanDetailSchema = Schema.Struct({ + id: Schema.String, + traceDbId: Schema.String, + traceId: Schema.String, + spanId: Schema.String, + parentSpanId: Schema.NullOr(Schema.String), + environmentId: Schema.String, + projectId: Schema.String, + organizationId: Schema.String, + startTime: Schema.String, + endTime: Schema.String, + durationMs: Schema.NullOr(Schema.Number), + name: Schema.String, + kind: Schema.Number, + statusCode: Schema.NullOr(Schema.Number), + statusMessage: Schema.NullOr(Schema.String), + model: Schema.NullOr(Schema.String), + provider: Schema.NullOr(Schema.String), + inputTokens: Schema.NullOr(Schema.Number), + outputTokens: Schema.NullOr(Schema.Number), + totalTokens: Schema.NullOr(Schema.Number), + costUsd: Schema.NullOr(Schema.Number), + functionId: Schema.NullOr(Schema.String), + functionName: Schema.NullOr(Schema.String), + functionVersion: Schema.NullOr(Schema.String), + errorType: Schema.NullOr(Schema.String), + errorMessage: Schema.NullOr(Schema.String), + attributes: Schema.String, + events: Schema.NullOr(Schema.String), + links: Schema.NullOr(Schema.String), + serviceName: Schema.NullOr(Schema.String), + serviceVersion: Schema.NullOr(Schema.String), + resourceAttributes: Schema.NullOr(Schema.String), +}); + +export type SpanDetail = typeof SpanDetailSchema.Type; + +export const TraceDetailResponseSchema = Schema.Struct({ + traceId: Schema.String, + spans: Schema.Array(SpanDetailSchema), + rootSpanId: Schema.NullOr(Schema.String), + totalDurationMs: Schema.NullOr(Schema.Number), +}); + +export type TraceDetailResponse = typeof TraceDetailResponseSchema.Type; + +// ============================================================================= +// Analytics Summary Schemas +// ============================================================================= + +export const AnalyticsSummaryRequestSchema = Schema.Struct({ + startTime: Schema.String, + endTime: Schema.String, + functionId: Schema.optional(Schema.String), +}); + +export type AnalyticsSummaryRequest = typeof AnalyticsSummaryRequestSchema.Type; + +const TopModelSchema = Schema.Struct({ + model: Schema.String, + count: Schema.Number, +}); + +const TopFunctionSchema = Schema.Struct({ + functionName: Schema.String, + count: Schema.Number, +}); + +export const AnalyticsSummaryResponseSchema = Schema.Struct({ + totalSpans: Schema.Number, + avgDurationMs: Schema.NullOr(Schema.Number), + p50DurationMs: Schema.NullOr(Schema.Number), + p95DurationMs: Schema.NullOr(Schema.Number), + p99DurationMs: Schema.NullOr(Schema.Number), + errorRate: Schema.Number, + totalTokens: Schema.Number, + totalCostUsd: Schema.Number, + topModels: Schema.Array(TopModelSchema), + topFunctions: Schema.Array(TopFunctionSchema), +}); + +export type AnalyticsSummaryResponse = + typeof AnalyticsSummaryResponseSchema.Type; diff --git a/cloud/api/search.test.ts b/cloud/api/search.test.ts new file mode 100644 index 0000000000..bbd2f5ef9f --- /dev/null +++ b/cloud/api/search.test.ts @@ -0,0 +1,519 @@ +import { Effect, Layer } from "effect"; +import { + describe, + it, + expect, + TestApiContext, + createApiClient, +} from "@/tests/api"; +import type { + ApiKeyInfo, + PublicEnvironment, + PublicProject, + PublicUser, +} from "@/db/schema"; +import { TEST_DATABASE_URL } from "@/tests/db"; +import { Authentication } from "@/auth"; +import { ClickHouseSearch } from "@/clickhouse/search"; +import { ClickHouse } from "@/clickhouse/client"; +import { SettingsService, getSettings } from "@/settings"; +import { searchHandler } from "@/api/search.handlers"; +import type { + SearchRequest, + SearchResponse, + TraceDetailResponse, + AnalyticsSummaryResponse, +} from "@/api/search.schemas"; + +describe.sequential("Search API", (it) => { + let project: PublicProject; + let environment: PublicEnvironment; + let apiKeyClient: Awaited>["client"]; + let disposeApiKeyClient: (() => Promise) | null = null; + let apiKeyInfo: ApiKeyInfo | undefined; + let ownerFromContext: PublicUser | null = null; + + it.effect( + "POST /organizations/:orgId/projects - create project for search test", + () => + Effect.gen(function* () { + const { client, org } = yield* TestApiContext; + project = yield* client.projects.create({ + path: { organizationId: org.id }, + payload: { name: "Search Test Project", slug: "search-test-project" }, + }); + expect(project.id).toBeDefined(); + }), + ); + + it.effect( + "POST /organizations/:orgId/projects/:projId/environments - create environment for search test", + () => + Effect.gen(function* () { + const { client, org } = yield* TestApiContext; + environment = yield* client.environments.create({ + path: { organizationId: org.id, projectId: project.id }, + payload: { + name: "Search Test Environment", + slug: "search-test-env", + }, + }); + expect(environment.id).toBeDefined(); + }), + ); + + it.effect("Create API key client for search tests", () => + Effect.gen(function* () { + const { org, owner } = yield* TestApiContext; + apiKeyInfo = { + apiKeyId: "test-search-api-key-id", + organizationId: org.id, + projectId: project.id, + environmentId: environment.id, + ownerId: owner.id, + ownerEmail: owner.email, + ownerName: owner.name, + ownerDeletedAt: owner.deletedAt, + }; + ownerFromContext = owner; + + const result = yield* Effect.promise(() => + createApiClient(TEST_DATABASE_URL, owner, apiKeyInfo), + ); + apiKeyClient = result.client; + disposeApiKeyClient = result.dispose; + }), + ); + + it.effect( + "POST /traces/search - returns empty results for new environment", + () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.search({ + payload: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + }, + }); + + expect(result.spans).toBeDefined(); + expect(Array.isArray(result.spans)).toBe(true); + expect(typeof result.total).toBe("number"); + expect(typeof result.hasMore).toBe("boolean"); + }), + ); + + it.effect("POST /traces/search - with query filter", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.search({ + payload: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + query: "test", + }, + }); + + expect(result.spans).toBeDefined(); + }), + ); + + it.effect("POST /traces/search - with model filter", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.search({ + payload: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + model: ["gpt-4"], + }, + }); + + expect(result.spans).toBeDefined(); + }), + ); + + it.effect("POST /traces/search - with provider filter", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.search({ + payload: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + provider: ["openai"], + }, + }); + + expect(result.spans).toBeDefined(); + }), + ); + + it.effect("POST /traces/search - with pagination", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.search({ + payload: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + limit: 10, + offset: 0, + }, + }); + + expect(result.spans).toBeDefined(); + }), + ); + + it.effect("POST /traces/search - with sorting", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.search({ + payload: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + sortBy: "duration_ms", + sortOrder: "desc", + }, + }); + + expect(result.spans).toBeDefined(); + }), + ); + + it.effect( + "searchHandler supports optional filters and maps attributeFilters", + () => + Effect.gen(function* () { + const settings = getSettings(); + const settingsLayer = Layer.succeed(SettingsService, { + ...settings, + env: "test", + }); + + if (!apiKeyInfo || !ownerFromContext) { + throw new Error("Missing API key context for search handler test"); + } + + const authenticationLayer = Layer.succeed(Authentication, { + user: ownerFromContext, + apiKeyInfo, + }); + + const clickHouseSearchLayer = ClickHouseSearch.Default.pipe( + Layer.provide(ClickHouse.Default), + Layer.provide(settingsLayer), + ); + + const result = yield* searchHandler({ + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + attributeFilters: [ + { + key: "span.type", + operator: "eq", + value: "server", + }, + ], + }).pipe( + Effect.provide( + Layer.mergeAll(authenticationLayer, clickHouseSearchLayer), + ), + ); + + expect(result.spans).toBeDefined(); + expect(typeof result.total).toBe("number"); + }), + ); + + it.effect("GET /traces/:traceId - returns empty for non-existent trace", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.getTraceDetail({ + path: { traceId: "00000000-0000-0000-0000-000000000000" }, + }); + + expect(result.traceId).toBe("00000000-0000-0000-0000-000000000000"); + expect(result.spans).toEqual([]); + }), + ); + + it.effect("GET /traces/analytics - returns analytics summary", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.getAnalyticsSummary({ + urlParams: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + }, + }); + + expect(typeof result.totalSpans).toBe("number"); + expect(typeof result.errorRate).toBe("number"); + expect(typeof result.totalTokens).toBe("number"); + expect(typeof result.totalCostUsd).toBe("number"); + expect(Array.isArray(result.topModels)).toBe(true); + expect(Array.isArray(result.topFunctions)).toBe(true); + }), + ); + + it.effect("GET /traces/analytics - with functionId filter", () => + Effect.gen(function* () { + const result = yield* apiKeyClient.traces.getAnalyticsSummary({ + urlParams: { + startTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date().toISOString(), + functionId: "00000000-0000-0000-0000-000000000001", + }, + }); + + expect(typeof result.totalSpans).toBe("number"); + }), + ); + + it.effect("Dispose API key client", () => + Effect.gen(function* () { + if (disposeApiKeyClient) { + yield* Effect.promise(disposeApiKeyClient); + } + }), + ); +}); + +describe("Search schema definitions", () => { + describe("SearchRequest", () => { + it("accepts valid search request with required fields", () => { + const input: SearchRequest = { + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-31T23:59:59Z", + }; + + expect(input.startTime).toBe("2024-01-01T00:00:00Z"); + expect(input.endTime).toBe("2024-01-31T23:59:59Z"); + }); + + it("accepts search request with optional filters", () => { + const input: SearchRequest = { + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-31T23:59:59Z", + query: "llm call", + model: ["gpt-4", "gpt-3.5-turbo"], + provider: ["openai"], + hasError: false, + limit: 100, + offset: 0, + sortBy: "start_time", + sortOrder: "desc", + }; + + expect(input.query).toBe("llm call"); + expect(input.model).toHaveLength(2); + expect(input.provider).toHaveLength(1); + expect(input.limit).toBe(100); + }); + + it("accepts search request with attribute filters", () => { + const input: SearchRequest = { + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-31T23:59:59Z", + attributeFilters: [ + { key: "gen_ai.request.model", operator: "eq", value: "gpt-4" }, + { key: "custom.tag", operator: "exists" }, + ], + }; + + expect(input.attributeFilters).toHaveLength(2); + expect(input.attributeFilters?.[0]?.operator).toBe("eq"); + expect(input.attributeFilters?.[1]?.operator).toBe("exists"); + }); + + it("accepts all valid sortBy values", () => { + const validSortByValues: SearchRequest["sortBy"][] = [ + "start_time", + "duration_ms", + "total_tokens", + ]; + + for (const sortBy of validSortByValues) { + const input: SearchRequest = { + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-31T23:59:59Z", + sortBy, + }; + expect(input.sortBy).toBe(sortBy); + } + }); + + it("accepts all valid sortOrder values", () => { + const validSortOrderValues: SearchRequest["sortOrder"][] = [ + "asc", + "desc", + ]; + + for (const sortOrder of validSortOrderValues) { + const input: SearchRequest = { + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-31T23:59:59Z", + sortOrder, + }; + expect(input.sortOrder).toBe(sortOrder); + } + }); + }); + + describe("SearchResponse", () => { + it("validates response structure", () => { + const response: SearchResponse = { + spans: [ + { + id: "span-id-1", + traceId: "trace-id-1", + spanId: "otel-span-id-1", + name: "llm.call", + startTime: "2024-01-15T10:00:00Z", + durationMs: 1500, + model: "gpt-4", + provider: "openai", + totalTokens: 150, + functionId: "00000000-0000-0000-0000-000000000001", + functionName: "my_function", + }, + ], + total: 1, + hasMore: false, + }; + + expect(response.spans).toHaveLength(1); + expect(response.spans[0]?.name).toBe("llm.call"); + expect(response.total).toBe(1); + expect(response.hasMore).toBe(false); + }); + + it("allows null values for optional span fields", () => { + const response: SearchResponse = { + spans: [ + { + id: "span-id-1", + traceId: "trace-id-1", + spanId: "otel-span-id-1", + name: "http.request", + startTime: "2024-01-15T10:00:00Z", + durationMs: null, + model: null, + provider: null, + totalTokens: null, + functionId: null, + functionName: null, + }, + ], + total: 1, + hasMore: false, + }; + + expect(response.spans[0]?.durationMs).toBeNull(); + expect(response.spans[0]?.model).toBeNull(); + }); + }); + + describe("TraceDetailResponse", () => { + it("validates trace detail response structure", () => { + const response: TraceDetailResponse = { + traceId: "trace-id-1", + spans: [ + { + id: "span-id-1", + traceDbId: "trace-db-id-1", + traceId: "trace-id-1", + spanId: "otel-span-id-1", + parentSpanId: null, + environmentId: "env-id-1", + projectId: "project-id-1", + organizationId: "org-id-1", + startTime: "2024-01-15T10:00:00Z", + endTime: "2024-01-15T10:00:01Z", + durationMs: 1000, + name: "root.span", + kind: 1, + statusCode: 0, + statusMessage: null, + model: "gpt-4", + provider: "openai", + inputTokens: 100, + outputTokens: 50, + totalTokens: 150, + costUsd: 0.01, + functionId: null, + functionName: null, + functionVersion: null, + errorType: null, + errorMessage: null, + attributes: "{}", + events: null, + links: null, + serviceName: "my-service", + serviceVersion: "1.0.0", + resourceAttributes: null, + }, + ], + rootSpanId: "otel-span-id-1", + totalDurationMs: 1000, + }; + + expect(response.traceId).toBe("trace-id-1"); + expect(response.spans).toHaveLength(1); + expect(response.rootSpanId).toBe("otel-span-id-1"); + expect(response.totalDurationMs).toBe(1000); + }); + + it("allows null rootSpanId and totalDurationMs for empty traces", () => { + const response: TraceDetailResponse = { + traceId: "non-existent-trace", + spans: [], + rootSpanId: null, + totalDurationMs: null, + }; + + expect(response.spans).toHaveLength(0); + expect(response.rootSpanId).toBeNull(); + expect(response.totalDurationMs).toBeNull(); + }); + }); + + describe("AnalyticsSummaryResponse", () => { + it("validates analytics summary response structure", () => { + const response: AnalyticsSummaryResponse = { + totalSpans: 1000, + avgDurationMs: 500.5, + p50DurationMs: 400, + p95DurationMs: 1200, + p99DurationMs: 2000, + errorRate: 0.05, + totalTokens: 150000, + totalCostUsd: 15.5, + topModels: [ + { model: "gpt-4", count: 600 }, + { model: "gpt-3.5-turbo", count: 400 }, + ], + topFunctions: [ + { functionName: "my_function", count: 200 }, + { functionName: "another_function", count: 100 }, + ], + }; + + expect(response.totalSpans).toBe(1000); + expect(response.avgDurationMs).toBe(500.5); + expect(response.topModels).toHaveLength(2); + expect(response.topFunctions).toHaveLength(2); + }); + + it("allows empty arrays for topModels and topFunctions", () => { + const response: AnalyticsSummaryResponse = { + totalSpans: 0, + avgDurationMs: null, + p50DurationMs: null, + p95DurationMs: null, + p99DurationMs: null, + errorRate: 0, + totalTokens: 0, + totalCostUsd: 0, + topModels: [], + topFunctions: [], + }; + + expect(response.topModels).toHaveLength(0); + expect(response.topFunctions).toHaveLength(0); + }); + }); +}); diff --git a/cloud/api/traces.schemas.ts b/cloud/api/traces.schemas.ts index 325cd96377..aa66f9c6f5 100644 --- a/cloud/api/traces.schemas.ts +++ b/cloud/api/traces.schemas.ts @@ -6,7 +6,15 @@ import { DatabaseError, AlreadyExistsError, UnauthorizedError, + ClickHouseError, } from "@/errors"; +import { + AnalyticsSummaryRequestSchema, + AnalyticsSummaryResponseSchema, + SearchRequestSchema, + SearchResponseSchema, + TraceDetailResponseSchema, +} from "@/api/search.schemas"; export const KeyValueSchema = Schema.Struct({ key: Schema.String, @@ -110,13 +118,54 @@ export const CreateTraceResponseSchema = Schema.Struct({ export type CreateTraceResponse = typeof CreateTraceResponseSchema.Type; -export class TracesApi extends HttpApiGroup.make("traces").add( - HttpApiEndpoint.post("create", "/traces") - .setPayload(CreateTraceRequestSchema) - .addSuccess(CreateTraceResponseSchema) - .addError(UnauthorizedError, { status: UnauthorizedError.status }) - .addError(NotFoundError, { status: NotFoundError.status }) - .addError(PermissionDeniedError, { status: PermissionDeniedError.status }) - .addError(DatabaseError, { status: DatabaseError.status }) - .addError(AlreadyExistsError, { status: AlreadyExistsError.status }), -) {} +export class TracesApi extends HttpApiGroup.make("traces") + .add( + HttpApiEndpoint.post("create", "/traces") + .setPayload(CreateTraceRequestSchema) + .addSuccess(CreateTraceResponseSchema) + .addError(UnauthorizedError, { status: UnauthorizedError.status }) + .addError(NotFoundError, { status: NotFoundError.status }) + .addError(PermissionDeniedError, { + status: PermissionDeniedError.status, + }) + .addError(DatabaseError, { status: DatabaseError.status }) + .addError(AlreadyExistsError, { status: AlreadyExistsError.status }), + ) + .add( + HttpApiEndpoint.post("search", "/traces/search") + .setPayload(SearchRequestSchema) + .addSuccess(SearchResponseSchema) + .addError(UnauthorizedError, { status: UnauthorizedError.status }) + .addError(PermissionDeniedError, { + status: PermissionDeniedError.status, + }) + .addError(ClickHouseError, { status: ClickHouseError.status }) + .addError(DatabaseError, { status: DatabaseError.status }), + ) + .add( + HttpApiEndpoint.get("getTraceDetail", "/traces/:traceId") + .setPath( + Schema.Struct({ + traceId: Schema.String, + }), + ) + .addSuccess(TraceDetailResponseSchema) + .addError(UnauthorizedError, { status: UnauthorizedError.status }) + .addError(NotFoundError, { status: NotFoundError.status }) + .addError(PermissionDeniedError, { + status: PermissionDeniedError.status, + }) + .addError(ClickHouseError, { status: ClickHouseError.status }) + .addError(DatabaseError, { status: DatabaseError.status }), + ) + .add( + HttpApiEndpoint.get("getAnalyticsSummary", "/traces/analytics") + .setUrlParams(AnalyticsSummaryRequestSchema) + .addSuccess(AnalyticsSummaryResponseSchema) + .addError(UnauthorizedError, { status: UnauthorizedError.status }) + .addError(PermissionDeniedError, { + status: PermissionDeniedError.status, + }) + .addError(ClickHouseError, { status: ClickHouseError.status }) + .addError(DatabaseError, { status: DatabaseError.status }), + ) {} diff --git a/cloud/app/routes/api.v0.$.tsx b/cloud/app/routes/api.v0.$.tsx index bf6e68a9f8..5ffd660848 100644 --- a/cloud/app/routes/api.v0.$.tsx +++ b/cloud/app/routes/api.v0.$.tsx @@ -1,10 +1,13 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Effect } from "effect"; +import { Effect, Layer } from "effect"; import { handleRequest } from "@/api/handler"; import { handleErrors, handleDefects } from "@/api/utils"; import { NotFoundError, InternalError } from "@/errors"; import { authenticate, type PathParameters } from "@/auth"; import { Database } from "@/db"; +import { ClickHouse } from "@/clickhouse/client"; +import { ClickHouseSearch } from "@/clickhouse/search"; +import { SettingsService, getSettings } from "@/settings"; /** * Extract path parameters from the splat path for API key validation. @@ -61,11 +64,14 @@ export const Route = createFileRoute("/api/v0/$")({ const pathParams = extractPathParameters(params["*"]); const authResult = yield* authenticate(request, pathParams); + const clickHouseSearch = yield* ClickHouseSearch; + const result = yield* handleRequest(request, { prefix: "/api/v0", user: authResult.user, apiKeyInfo: authResult.apiKeyInfo, environment: process.env.ENVIRONMENT || "development", + clickHouseSearch, }); if (!result.matched) { @@ -75,14 +81,25 @@ export const Route = createFileRoute("/api/v0/$")({ return result.response; }).pipe( Effect.provide( - Database.Live({ - database: { connectionString: databaseUrl }, - payments: { - apiKey: process.env.STRIPE_SECRET_KEY || "", - routerPriceId: process.env.STRIPE_ROUTER_PRICE_ID || "", - routerMeterId: process.env.STRIPE_ROUTER_METER_ID || "", - }, - }), + Layer.mergeAll( + Database.Live({ + database: { connectionString: databaseUrl }, + payments: { + apiKey: process.env.STRIPE_SECRET_KEY || "", + routerPriceId: process.env.STRIPE_ROUTER_PRICE_ID || "", + routerMeterId: process.env.STRIPE_ROUTER_METER_ID || "", + }, + }), + ClickHouseSearch.Default.pipe( + Layer.provide(ClickHouse.Default), + Layer.provide( + Layer.succeed(SettingsService, { + ...getSettings(), + env: process.env.ENVIRONMENT || "development", + }), + ), + ), + ), ), handleErrors, handleDefects, diff --git a/cloud/tests/api.ts b/cloud/tests/api.ts index 33faf72fc9..70612761e8 100644 --- a/cloud/tests/api.ts +++ b/cloud/tests/api.ts @@ -16,11 +16,13 @@ import { HttpApiBuilder, HttpServer, } from "@effect/platform"; -import { SettingsService } from "@/settings"; +import { SettingsService, getSettings } from "@/settings"; import { Database } from "@/db"; import { DrizzleORM } from "@/db/client"; import { Payments } from "@/payments"; import { AuthenticatedUser, Authentication } from "@/auth"; +import { ClickHouse } from "@/clickhouse/client"; +import { ClickHouseSearch } from "@/clickhouse/search"; import type { AuthResult } from "@/auth/context"; import type { PublicUser, PublicOrganization, ApiKeyInfo } from "@/db/schema"; import { users } from "@/db/schema"; @@ -29,6 +31,8 @@ import { DefaultMockPayments } from "@/tests/payments"; import type { StreamMeteringContext } from "@/api/router/streaming"; import type { ProviderName } from "@/api/router/providers"; import { eq } from "drizzle-orm"; +import { CLICKHOUSE_CONNECTION_FILE } from "@/tests/global-setup"; +import fs from "fs"; // Re-export expect from vitest export { expect }; @@ -56,6 +60,25 @@ function createTestDatabaseLayer(connectionString: string) { */ const TestDatabaseLayer = createTestDatabaseLayer(TEST_DATABASE_URL); +type ClickHouseConnectionFile = { + url: string; + user: string; + password: string; + database: string; + nativePort: number; +}; + +function getTestClickHouseConfig(): ClickHouseConnectionFile { + try { + const raw = fs.readFileSync(CLICKHOUSE_CONNECTION_FILE, "utf-8"); + return JSON.parse(raw) as ClickHouseConnectionFile; + } catch { + throw new Error( + "TEST_CLICKHOUSE_URL not set. Ensure global-setup.ts ran successfully.", + ); + } +} + /** * Wraps a test function to automatically provide Database and Payments layers. * @@ -120,11 +143,28 @@ function createTestWebHandler( user: PublicUser, apiKeyInfo?: ApiKeyInfo, ) { + // ClickHouse services layer for test environment + const clickhouseConfig = getTestClickHouseConfig(); + const settings = getSettings(); + const settingsLayer = Layer.succeed(SettingsService, { + ...settings, + env: "test", + CLICKHOUSE_URL: clickhouseConfig.url, + CLICKHOUSE_USER: clickhouseConfig.user, + CLICKHOUSE_PASSWORD: clickhouseConfig.password, + CLICKHOUSE_DATABASE: clickhouseConfig.database, + }); + const clickHouseSearchLayer = ClickHouseSearch.Default.pipe( + Layer.provide(ClickHouse.Default), + Layer.provide(settingsLayer), + ); + const services = Layer.mergeAll( - Layer.succeed(SettingsService, { env: "test" }), + settingsLayer, Layer.succeed(AuthenticatedUser, user), Layer.succeed(Authentication, { user, apiKeyInfo }), createTestDatabaseLayer(databaseUrl), + clickHouseSearchLayer, ); const ApiWithDependencies = Layer.merge( @@ -391,11 +431,25 @@ function createSimpleTestWebHandler() { const databaseUrl = TEST_DATABASE_URL; const authResult: AuthResult = { user: mockUser }; + // ClickHouse services layer for test environment + const settings = getSettings(); + const settingsLayer = Layer.succeed(SettingsService, { + ...settings, + env: "test", + }); + const clickHouseClientLayer = ClickHouse.Default.pipe( + Layer.provide(settingsLayer), + ); + const clickHouseSearchLayer = ClickHouseSearch.Default.pipe( + Layer.provide(clickHouseClientLayer), + ); + const services = Layer.mergeAll( - Layer.succeed(SettingsService, { env: "test" }), + settingsLayer, Layer.succeed(AuthenticatedUser, mockUser), Layer.succeed(Authentication, authResult), createTestDatabaseLayer(databaseUrl).pipe(Layer.orDie), + clickHouseSearchLayer, ); const ApiWithDependencies = Layer.mergeAll( diff --git a/cloud/tests/clickhouse.ts b/cloud/tests/clickhouse.ts index 0ea9638433..20a4dc64ec 100644 --- a/cloud/tests/clickhouse.ts +++ b/cloud/tests/clickhouse.ts @@ -3,20 +3,36 @@ import { describe, expect, it as vitestIt } from "@effect/vitest"; import { createCustomIt } from "@/tests/shared"; import { ClickHouse } from "@/clickhouse/client"; import { SettingsService, type Settings } from "@/settings"; +import { CLICKHOUSE_CONNECTION_FILE } from "@/tests/global-setup"; +import fs from "fs"; // Re-export describe and expect for convenience export { describe, expect }; -// Environment variables for ClickHouse connection -const CLICKHOUSE_URL = process.env.CLICKHOUSE_URL ?? "http://localhost:8123"; -const CLICKHOUSE_USER = process.env.CLICKHOUSE_USER ?? "default"; -const CLICKHOUSE_PASSWORD = process.env.CLICKHOUSE_PASSWORD ?? "clickhouse"; -const CLICKHOUSE_DATABASE = - process.env.CLICKHOUSE_DATABASE ?? "mirascope_analytics"; +type ClickHouseConnectionFile = { + url: string; + user: string; + password: string; + database: string; + nativePort: number; +}; + +function getTestClickHouseConfig(): ClickHouseConnectionFile { + try { + const raw = fs.readFileSync(CLICKHOUSE_CONNECTION_FILE, "utf-8"); + return JSON.parse(raw) as ClickHouseConnectionFile; + } catch { + throw new Error( + "TEST_CLICKHOUSE_URL not set. Ensure global-setup.ts ran successfully.", + ); + } +} + +const clickhouseConfig = getTestClickHouseConfig(); export const checkClickHouseAvailable = async (): Promise => { try { - const response = await fetch(`${CLICKHOUSE_URL}/ping`, { + const response = await fetch(`${clickhouseConfig.url}/ping`, { method: "GET", signal: AbortSignal.timeout(2000), }); @@ -33,10 +49,10 @@ export const clickHouseAvailable = await checkClickHouseAvailable(); */ const testSettings: Settings = { env: "local", - CLICKHOUSE_URL, - CLICKHOUSE_USER, - CLICKHOUSE_PASSWORD, - CLICKHOUSE_DATABASE, + CLICKHOUSE_URL: clickhouseConfig.url, + CLICKHOUSE_USER: clickhouseConfig.user, + CLICKHOUSE_PASSWORD: clickhouseConfig.password, + CLICKHOUSE_DATABASE: clickhouseConfig.database, CLICKHOUSE_TLS_ENABLED: false, }; diff --git a/cloud/tests/global-setup.ts b/cloud/tests/global-setup.ts index 72417bc54a..a63c3c0849 100644 --- a/cloud/tests/global-setup.ts +++ b/cloud/tests/global-setup.ts @@ -25,6 +25,10 @@ export const CONNECTION_FILE = path.join( os.tmpdir(), "mirascope-test-db-url.txt", ); +export const CLICKHOUSE_CONNECTION_FILE = path.join( + os.tmpdir(), + "mirascope-test-clickhouse-connection.json", +); // Custom error type for container failures class ContainerError extends Data.TaggedError("ContainerError")<{ @@ -64,36 +68,50 @@ const clickhouseUser = process.env.TEST_CLICKHOUSE_USER ?? "default"; const clickhousePassword = process.env.TEST_CLICKHOUSE_PASSWORD ?? randomUUID(); const clickhouseDatabase = "mirascope_analytics"; -const runClickhouseMigrations = (clickhouseUrl: string, nativePort: number) => { - execFileSync("bash", ["clickhouse/migrate.sh", "migrate"], { - cwd: path.resolve(__dirname, ".."), - env: { - ...process.env, - TZ: "UTC", - CLICKHOUSE_URL: clickhouseUrl, - CLICKHOUSE_USER: clickhouseUser, - CLICKHOUSE_PASSWORD: clickhousePassword, - CLICKHOUSE_DATABASE: clickhouseDatabase, - CLICKHOUSE_MIGRATE_NATIVE_PORT: String(nativePort), +const acquireClickhouseContainer = Effect.tryPromise({ + try: () => + new GenericContainer(clickhouseImage) + .withExposedPorts(8123, 9000) + .withEnvironment({ + CLICKHOUSE_DB: clickhouseDatabase, + CLICKHOUSE_USER: clickhouseUser, + CLICKHOUSE_PASSWORD: clickhousePassword, + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1", + }) + .withWaitStrategy(Wait.forHttp("/ping", 8123)) + .start(), + catch: (cause) => new ContainerError({ cause }), +}); + +const runClickhouseMigrations = (clickhouseUrl: string, nativePort: number) => + Effect.try({ + try: () => { + execFileSync("bash", ["clickhouse/migrate.sh", "migrate"], { + cwd: path.resolve(__dirname, ".."), + env: { + ...process.env, + TZ: "UTC", + CLICKHOUSE_URL: clickhouseUrl, + CLICKHOUSE_USER: clickhouseUser, + CLICKHOUSE_PASSWORD: clickhousePassword, + CLICKHOUSE_DATABASE: clickhouseDatabase, + CLICKHOUSE_MIGRATE_NATIVE_PORT: String(nativePort), + }, + stdio: "inherit", + }); }, - stdio: "inherit", + catch: (cause) => new ContainerError({ cause }), }); -}; // Vitest global setup - runs once before all tests export async function setup() { const scope = Effect.runSync(Scope.make()); - clickhouseContainer = await new GenericContainer(clickhouseImage) - .withExposedPorts(8123, 9000) - .withEnvironment({ - CLICKHOUSE_DB: clickhouseDatabase, - CLICKHOUSE_USER: clickhouseUser, - CLICKHOUSE_PASSWORD: clickhousePassword, - CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1", - }) - .withWaitStrategy(Wait.forHttp("/ping", 8123)) - .start(); + clickhouseContainer = await Effect.runPromise( + Effect.acquireRelease(acquireClickhouseContainer, (c) => + Effect.promise(() => c.stop()), + ).pipe(Scope.extend(scope)), + ); const clickhouseHttpPort = clickhouseContainer.getMappedPort(8123); const clickhouseNativePort = clickhouseContainer.getMappedPort(9000); @@ -105,7 +123,23 @@ export async function setup() { process.env.CLICKHOUSE_DATABASE = clickhouseDatabase; process.env.CLICKHOUSE_MIGRATE_NATIVE_PORT = String(clickhouseNativePort); - runClickhouseMigrations(clickhouseUrl, clickhouseNativePort); + Effect.runSync(runClickhouseMigrations(clickhouseUrl, clickhouseNativePort)); + + fs.writeFileSync( + CLICKHOUSE_CONNECTION_FILE, + JSON.stringify( + { + url: clickhouseUrl, + user: clickhouseUser, + password: clickhousePassword, + database: clickhouseDatabase, + nativePort: clickhouseNativePort, + }, + null, + 2, + ), + "utf-8", + ); container = await Effect.runPromise( Effect.acquireRelease(acquireContainer, (c) => @@ -133,6 +167,11 @@ export async function teardown() { } catch { // Ignore if file doesn't exist } + try { + fs.unlinkSync(CLICKHOUSE_CONNECTION_FILE); + } catch { + // Ignore if file doesn't exist + } // Stop the container if (container) { diff --git a/fern/openapi.json b/fern/openapi.json index 19d37a6c59..877960b087 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -551,6 +551,898 @@ } } }, + "/traces/search": { + "post": { + "tags": [ + "traces" + ], + "operationId": "traces.search", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "spans", + "total", + "hasMore" + ], + "properties": { + "spans": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "traceId", + "spanId", + "name", + "startTime", + "durationMs", + "model", + "provider", + "totalTokens", + "functionId", + "functionName" + ], + "properties": { + "id": { + "type": "string" + }, + "traceId": { + "type": "string" + }, + "spanId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "durationMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "totalTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "functionId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "functionName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "total": { + "type": "number" + }, + "hasMore": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "PermissionDeniedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionDeniedError" + } + } + } + }, + "500": { + "description": "ClickHouseError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ClickHouseError" + }, + { + "$ref": "#/components/schemas/DatabaseError" + } + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "startTime", + "endTime" + ], + "properties": { + "startTime": { + "type": "string" + }, + "endTime": { + "type": "string" + }, + "query": { + "type": "string" + }, + "traceId": { + "type": "string" + }, + "spanId": { + "type": "string" + }, + "model": { + "type": "array", + "items": { + "type": "string" + } + }, + "provider": { + "type": "array", + "items": { + "type": "string" + } + }, + "functionId": { + "type": "string" + }, + "functionName": { + "type": "string" + }, + "hasError": { + "type": "boolean" + }, + "minTokens": { + "type": "number" + }, + "maxTokens": { + "type": "number" + }, + "minDuration": { + "type": "number" + }, + "maxDuration": { + "type": "number" + }, + "attributeFilters": { + "type": "array", + "items": { + "type": "object", + "required": [ + "key", + "operator" + ], + "properties": { + "key": { + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq", + "contains", + "exists" + ] + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "limit": { + "type": "number" + }, + "offset": { + "type": "number" + }, + "sortBy": { + "type": "string", + "enum": [ + "start_time", + "duration_ms", + "total_tokens" + ] + }, + "sortOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/traces/{traceId}": { + "get": { + "tags": [ + "traces" + ], + "operationId": "traces.getTraceDetail", + "parameters": [ + { + "name": "traceId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "spans", + "rootSpanId", + "totalDurationMs" + ], + "properties": { + "traceId": { + "type": "string" + }, + "spans": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "traceDbId", + "traceId", + "spanId", + "parentSpanId", + "environmentId", + "projectId", + "organizationId", + "startTime", + "endTime", + "durationMs", + "name", + "kind", + "statusCode", + "statusMessage", + "model", + "provider", + "inputTokens", + "outputTokens", + "totalTokens", + "costUsd", + "functionId", + "functionName", + "functionVersion", + "errorType", + "errorMessage", + "attributes", + "events", + "links", + "serviceName", + "serviceVersion", + "resourceAttributes" + ], + "properties": { + "id": { + "type": "string" + }, + "traceDbId": { + "type": "string" + }, + "traceId": { + "type": "string" + }, + "spanId": { + "type": "string" + }, + "parentSpanId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "environmentId": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "endTime": { + "type": "string" + }, + "durationMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "kind": { + "type": "number" + }, + "statusCode": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "statusMessage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "inputTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "totalTokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "costUsd": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "functionId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "functionName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "functionVersion": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "errorType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "errorMessage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "attributes": { + "type": "string" + }, + "events": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "links": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "serviceName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "serviceVersion": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "resourceAttributes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "rootSpanId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "totalDurationMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "PermissionDeniedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionDeniedError" + } + } + } + }, + "404": { + "description": "NotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + }, + "500": { + "description": "ClickHouseError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ClickHouseError" + }, + { + "$ref": "#/components/schemas/DatabaseError" + } + ] + } + } + } + } + } + } + }, + "/traces/analytics": { + "get": { + "tags": [ + "traces" + ], + "operationId": "traces.getAnalyticsSummary", + "parameters": [ + { + "name": "startTime", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "endTime", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "functionId", + "in": "query", + "schema": { + "type": "string" + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "totalSpans", + "avgDurationMs", + "p50DurationMs", + "p95DurationMs", + "p99DurationMs", + "errorRate", + "totalTokens", + "totalCostUsd", + "topModels", + "topFunctions" + ], + "properties": { + "totalSpans": { + "type": "number" + }, + "avgDurationMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "p50DurationMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "p95DurationMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "p99DurationMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "errorRate": { + "type": "number" + }, + "totalTokens": { + "type": "number" + }, + "totalCostUsd": { + "type": "number" + }, + "topModels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "model", + "count" + ], + "properties": { + "model": { + "type": "string" + }, + "count": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "topFunctions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "functionName", + "count" + ], + "properties": { + "functionName": { + "type": "string" + }, + "count": { + "type": "number" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "PermissionDeniedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionDeniedError" + } + } + } + }, + "500": { + "description": "ClickHouseError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ClickHouseError" + }, + { + "$ref": "#/components/schemas/DatabaseError" + } + ] + } + } + } + } + } + } + }, "/docs/openapi.json": { "get": { "tags": [ @@ -5272,6 +6164,29 @@ }, "additionalProperties": false }, + "ClickHouseError": { + "type": "object", + "required": [ + "message", + "tag" + ], + "properties": { + "message": { + "type": "string" + }, + "cause": { + "$id": "/schemas/unknown", + "title": "unknown" + }, + "tag": { + "type": "string", + "enum": [ + "ClickHouseError" + ] + } + }, + "additionalProperties": false + }, "StripeError": { "type": "object", "required": [ diff --git a/python/mirascope/api/_generated/__init__.py b/python/mirascope/api/_generated/__init__.py index 63c09ad37f..168721bbcc 100644 --- a/python/mirascope/api/_generated/__init__.py +++ b/python/mirascope/api/_generated/__init__.py @@ -5,6 +5,7 @@ from .types import ( AlreadyExistsError, AlreadyExistsErrorTag, + ClickHouseError, DatabaseError, DatabaseErrorTag, HttpApiDecodeError, @@ -34,7 +35,17 @@ NotFoundError, UnauthorizedError, ) -from . import annotations, api_keys, docs, environments, functions, health, organizations, projects, traces +from . import ( + annotations, + api_keys, + docs, + environments, + functions, + health, + organizations, + projects, + traces, +) from .annotations import ( AnnotationsCreateRequestLabel, AnnotationsCreateResponse, @@ -82,7 +93,12 @@ OrganizationsUpdateResponse, OrganizationsUpdateResponseRole, ) -from .projects import ProjectsCreateResponse, ProjectsGetResponse, ProjectsListResponseItem, ProjectsUpdateResponse +from .projects import ( + ProjectsCreateResponse, + ProjectsGetResponse, + ProjectsListResponseItem, + ProjectsUpdateResponse, +) from .traces import ( TracesCreateRequestResourceSpansItem, TracesCreateRequestResourceSpansItemResource, @@ -107,6 +123,17 @@ TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus, TracesCreateResponse, TracesCreateResponsePartialSuccess, + TracesGetAnalyticsSummaryResponse, + TracesGetAnalyticsSummaryResponseTopFunctionsItem, + TracesGetAnalyticsSummaryResponseTopModelsItem, + TracesGetTraceDetailResponse, + TracesGetTraceDetailResponseSpansItem, + TracesSearchRequestAttributeFiltersItem, + TracesSearchRequestAttributeFiltersItemOperator, + TracesSearchRequestSortBy, + TracesSearchRequestSortOrder, + TracesSearchResponse, + TracesSearchResponseSpansItem, ) __all__ = [ @@ -129,6 +156,7 @@ "ApiKeysListResponseItem", "AsyncMirascope", "BadRequestError", + "ClickHouseError", "ConflictError", "DatabaseError", "DatabaseErrorTag", @@ -205,6 +233,17 @@ "TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus", "TracesCreateResponse", "TracesCreateResponsePartialSuccess", + "TracesGetAnalyticsSummaryResponse", + "TracesGetAnalyticsSummaryResponseTopFunctionsItem", + "TracesGetAnalyticsSummaryResponseTopModelsItem", + "TracesGetTraceDetailResponse", + "TracesGetTraceDetailResponseSpansItem", + "TracesSearchRequestAttributeFiltersItem", + "TracesSearchRequestAttributeFiltersItemOperator", + "TracesSearchRequestSortBy", + "TracesSearchRequestSortOrder", + "TracesSearchResponse", + "TracesSearchResponseSpansItem", "UnauthorizedError", "UnauthorizedErrorBody", "UnauthorizedErrorTag", diff --git a/python/mirascope/api/_generated/client.py b/python/mirascope/api/_generated/client.py index 976ce8b0ec..a8ac7778bc 100644 --- a/python/mirascope/api/_generated/client.py +++ b/python/mirascope/api/_generated/client.py @@ -60,13 +60,19 @@ def __init__( httpx_client: typing.Optional[httpx.Client] = None, ): _defaulted_timeout = ( - timeout if timeout is not None else 180 if httpx_client is None else httpx_client.timeout.read + timeout + if timeout is not None + else 180 + if httpx_client is None + else httpx_client.timeout.read ) self._client_wrapper = SyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), httpx_client=httpx_client if httpx_client is not None - else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + else httpx.Client( + timeout=_defaulted_timeout, follow_redirects=follow_redirects + ) if follow_redirects is not None else httpx.Client(timeout=_defaulted_timeout), timeout=_defaulted_timeout, @@ -126,13 +132,19 @@ def __init__( httpx_client: typing.Optional[httpx.AsyncClient] = None, ): _defaulted_timeout = ( - timeout if timeout is not None else 180 if httpx_client is None else httpx_client.timeout.read + timeout + if timeout is not None + else 180 + if httpx_client is None + else httpx_client.timeout.read ) self._client_wrapper = AsyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), httpx_client=httpx_client if httpx_client is not None - else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + else httpx.AsyncClient( + timeout=_defaulted_timeout, follow_redirects=follow_redirects + ) if follow_redirects is not None else httpx.AsyncClient(timeout=_defaulted_timeout), timeout=_defaulted_timeout, @@ -140,7 +152,9 @@ def __init__( self.health = AsyncHealthClient(client_wrapper=self._client_wrapper) self.traces = AsyncTracesClient(client_wrapper=self._client_wrapper) self.docs = AsyncDocsClient(client_wrapper=self._client_wrapper) - self.organizations = AsyncOrganizationsClient(client_wrapper=self._client_wrapper) + self.organizations = AsyncOrganizationsClient( + client_wrapper=self._client_wrapper + ) self.projects = AsyncProjectsClient(client_wrapper=self._client_wrapper) self.environments = AsyncEnvironmentsClient(client_wrapper=self._client_wrapper) self.api_keys = AsyncApiKeysClient(client_wrapper=self._client_wrapper) @@ -148,10 +162,14 @@ def __init__( self.annotations = AsyncAnnotationsClient(client_wrapper=self._client_wrapper) -def _get_base_url(*, base_url: typing.Optional[str] = None, environment: MirascopeEnvironment) -> str: +def _get_base_url( + *, base_url: typing.Optional[str] = None, environment: MirascopeEnvironment +) -> str: if base_url is not None: return base_url elif environment is not None: return environment.value else: - raise Exception("Please pass in either base_url or environment to construct the client") + raise Exception( + "Please pass in either base_url or environment to construct the client" + ) diff --git a/python/mirascope/api/_generated/reference.md b/python/mirascope/api/_generated/reference.md index 848c63d181..a41a02b7d0 100644 --- a/python/mirascope/api/_generated/reference.md +++ b/python/mirascope/api/_generated/reference.md @@ -116,6 +116,330 @@ client.traces.create( + + + + +
client.traces.search(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from mirascope.api._generated import Mirascope + +client = Mirascope() +client.traces.search( + start_time="startTime", + end_time="endTime", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**start_time:** `str` + +
+
+ +
+
+ +**end_time:** `str` + +
+
+ +
+
+ +**query:** `typing.Optional[str]` + +
+
+ +
+
+ +**trace_id:** `typing.Optional[str]` + +
+
+ +
+
+ +**span_id:** `typing.Optional[str]` + +
+
+ +
+
+ +**model:** `typing.Optional[typing.Sequence[str]]` + +
+
+ +
+
+ +**provider:** `typing.Optional[typing.Sequence[str]]` + +
+
+ +
+
+ +**function_id:** `typing.Optional[str]` + +
+
+ +
+
+ +**function_name:** `typing.Optional[str]` + +
+
+ +
+
+ +**has_error:** `typing.Optional[bool]` + +
+
+ +
+
+ +**min_tokens:** `typing.Optional[float]` + +
+
+ +
+
+ +**max_tokens:** `typing.Optional[float]` + +
+
+ +
+
+ +**min_duration:** `typing.Optional[float]` + +
+
+ +
+
+ +**max_duration:** `typing.Optional[float]` + +
+
+ +
+
+ +**attribute_filters:** `typing.Optional[typing.Sequence[TracesSearchRequestAttributeFiltersItem]]` + +
+
+ +
+
+ +**limit:** `typing.Optional[float]` + +
+
+ +
+
+ +**offset:** `typing.Optional[float]` + +
+
+ +
+
+ +**sort_by:** `typing.Optional[TracesSearchRequestSortBy]` + +
+
+ +
+
+ +**sort_order:** `typing.Optional[TracesSearchRequestSortOrder]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.traces.gettracedetail(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from mirascope.api._generated import Mirascope + +client = Mirascope() +client.traces.gettracedetail( + trace_id="traceId", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**trace_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.traces.getanalyticssummary(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from mirascope.api._generated import Mirascope + +client = Mirascope() +client.traces.getanalyticssummary( + start_time="startTime", + end_time="endTime", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**start_time:** `str` + +
+
+ +
+
+ +**end_time:** `str` + +
+
+ +
+
+ +**function_id:** `typing.Optional[str]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/python/mirascope/api/_generated/traces/__init__.py b/python/mirascope/api/_generated/traces/__init__.py index 32cbe0d219..0373c10f95 100644 --- a/python/mirascope/api/_generated/traces/__init__.py +++ b/python/mirascope/api/_generated/traces/__init__.py @@ -26,6 +26,17 @@ TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus, TracesCreateResponse, TracesCreateResponsePartialSuccess, + TracesGetAnalyticsSummaryResponse, + TracesGetAnalyticsSummaryResponseTopFunctionsItem, + TracesGetAnalyticsSummaryResponseTopModelsItem, + TracesGetTraceDetailResponse, + TracesGetTraceDetailResponseSpansItem, + TracesSearchRequestAttributeFiltersItem, + TracesSearchRequestAttributeFiltersItemOperator, + TracesSearchRequestSortBy, + TracesSearchRequestSortOrder, + TracesSearchResponse, + TracesSearchResponseSpansItem, ) __all__ = [ @@ -52,4 +63,15 @@ "TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus", "TracesCreateResponse", "TracesCreateResponsePartialSuccess", + "TracesGetAnalyticsSummaryResponse", + "TracesGetAnalyticsSummaryResponseTopFunctionsItem", + "TracesGetAnalyticsSummaryResponseTopModelsItem", + "TracesGetTraceDetailResponse", + "TracesGetTraceDetailResponseSpansItem", + "TracesSearchRequestAttributeFiltersItem", + "TracesSearchRequestAttributeFiltersItemOperator", + "TracesSearchRequestSortBy", + "TracesSearchRequestSortOrder", + "TracesSearchResponse", + "TracesSearchResponseSpansItem", ] diff --git a/python/mirascope/api/_generated/traces/client.py b/python/mirascope/api/_generated/traces/client.py index cabb119595..6b13f220bd 100644 --- a/python/mirascope/api/_generated/traces/client.py +++ b/python/mirascope/api/_generated/traces/client.py @@ -5,8 +5,20 @@ from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from .raw_client import AsyncRawTracesClient, RawTracesClient -from .types.traces_create_request_resource_spans_item import TracesCreateRequestResourceSpansItem +from .types.traces_create_request_resource_spans_item import ( + TracesCreateRequestResourceSpansItem, +) from .types.traces_create_response import TracesCreateResponse +from .types.traces_get_analytics_summary_response import ( + TracesGetAnalyticsSummaryResponse, +) +from .types.traces_get_trace_detail_response import TracesGetTraceDetailResponse +from .types.traces_search_request_attribute_filters_item import ( + TracesSearchRequestAttributeFiltersItem, +) +from .types.traces_search_request_sort_by import TracesSearchRequestSortBy +from .types.traces_search_request_sort_order import TracesSearchRequestSortOrder +from .types.traces_search_response import TracesSearchResponse # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -76,7 +88,191 @@ def create( ], ) """ - _response = self._raw_client.create(resource_spans=resource_spans, request_options=request_options) + _response = self._raw_client.create( + resource_spans=resource_spans, request_options=request_options + ) + return _response.data + + def search( + self, + *, + start_time: str, + end_time: str, + query: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + span_id: typing.Optional[str] = OMIT, + model: typing.Optional[typing.Sequence[str]] = OMIT, + provider: typing.Optional[typing.Sequence[str]] = OMIT, + function_id: typing.Optional[str] = OMIT, + function_name: typing.Optional[str] = OMIT, + has_error: typing.Optional[bool] = OMIT, + min_tokens: typing.Optional[float] = OMIT, + max_tokens: typing.Optional[float] = OMIT, + min_duration: typing.Optional[float] = OMIT, + max_duration: typing.Optional[float] = OMIT, + attribute_filters: typing.Optional[ + typing.Sequence[TracesSearchRequestAttributeFiltersItem] + ] = OMIT, + limit: typing.Optional[float] = OMIT, + offset: typing.Optional[float] = OMIT, + sort_by: typing.Optional[TracesSearchRequestSortBy] = OMIT, + sort_order: typing.Optional[TracesSearchRequestSortOrder] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> TracesSearchResponse: + """ + Parameters + ---------- + start_time : str + + end_time : str + + query : typing.Optional[str] + + trace_id : typing.Optional[str] + + span_id : typing.Optional[str] + + model : typing.Optional[typing.Sequence[str]] + + provider : typing.Optional[typing.Sequence[str]] + + function_id : typing.Optional[str] + + function_name : typing.Optional[str] + + has_error : typing.Optional[bool] + + min_tokens : typing.Optional[float] + + max_tokens : typing.Optional[float] + + min_duration : typing.Optional[float] + + max_duration : typing.Optional[float] + + attribute_filters : typing.Optional[typing.Sequence[TracesSearchRequestAttributeFiltersItem]] + + limit : typing.Optional[float] + + offset : typing.Optional[float] + + sort_by : typing.Optional[TracesSearchRequestSortBy] + + sort_order : typing.Optional[TracesSearchRequestSortOrder] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TracesSearchResponse + Success + + Examples + -------- + from mirascope.api._generated import Mirascope + + client = Mirascope() + client.traces.search( + start_time="startTime", + end_time="endTime", + ) + """ + _response = self._raw_client.search( + start_time=start_time, + end_time=end_time, + query=query, + trace_id=trace_id, + span_id=span_id, + model=model, + provider=provider, + function_id=function_id, + function_name=function_name, + has_error=has_error, + min_tokens=min_tokens, + max_tokens=max_tokens, + min_duration=min_duration, + max_duration=max_duration, + attribute_filters=attribute_filters, + limit=limit, + offset=offset, + sort_by=sort_by, + sort_order=sort_order, + request_options=request_options, + ) + return _response.data + + def gettracedetail( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TracesGetTraceDetailResponse: + """ + Parameters + ---------- + trace_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TracesGetTraceDetailResponse + Success + + Examples + -------- + from mirascope.api._generated import Mirascope + + client = Mirascope() + client.traces.gettracedetail( + trace_id="traceId", + ) + """ + _response = self._raw_client.gettracedetail( + trace_id, request_options=request_options + ) + return _response.data + + def getanalyticssummary( + self, + *, + start_time: str, + end_time: str, + function_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TracesGetAnalyticsSummaryResponse: + """ + Parameters + ---------- + start_time : str + + end_time : str + + function_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TracesGetAnalyticsSummaryResponse + Success + + Examples + -------- + from mirascope.api._generated import Mirascope + + client = Mirascope() + client.traces.getanalyticssummary( + start_time="startTime", + end_time="endTime", + ) + """ + _response = self._raw_client.getanalyticssummary( + start_time=start_time, + end_time=end_time, + function_id=function_id, + request_options=request_options, + ) return _response.data @@ -152,5 +348,213 @@ async def main() -> None: asyncio.run(main()) """ - _response = await self._raw_client.create(resource_spans=resource_spans, request_options=request_options) + _response = await self._raw_client.create( + resource_spans=resource_spans, request_options=request_options + ) + return _response.data + + async def search( + self, + *, + start_time: str, + end_time: str, + query: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + span_id: typing.Optional[str] = OMIT, + model: typing.Optional[typing.Sequence[str]] = OMIT, + provider: typing.Optional[typing.Sequence[str]] = OMIT, + function_id: typing.Optional[str] = OMIT, + function_name: typing.Optional[str] = OMIT, + has_error: typing.Optional[bool] = OMIT, + min_tokens: typing.Optional[float] = OMIT, + max_tokens: typing.Optional[float] = OMIT, + min_duration: typing.Optional[float] = OMIT, + max_duration: typing.Optional[float] = OMIT, + attribute_filters: typing.Optional[ + typing.Sequence[TracesSearchRequestAttributeFiltersItem] + ] = OMIT, + limit: typing.Optional[float] = OMIT, + offset: typing.Optional[float] = OMIT, + sort_by: typing.Optional[TracesSearchRequestSortBy] = OMIT, + sort_order: typing.Optional[TracesSearchRequestSortOrder] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> TracesSearchResponse: + """ + Parameters + ---------- + start_time : str + + end_time : str + + query : typing.Optional[str] + + trace_id : typing.Optional[str] + + span_id : typing.Optional[str] + + model : typing.Optional[typing.Sequence[str]] + + provider : typing.Optional[typing.Sequence[str]] + + function_id : typing.Optional[str] + + function_name : typing.Optional[str] + + has_error : typing.Optional[bool] + + min_tokens : typing.Optional[float] + + max_tokens : typing.Optional[float] + + min_duration : typing.Optional[float] + + max_duration : typing.Optional[float] + + attribute_filters : typing.Optional[typing.Sequence[TracesSearchRequestAttributeFiltersItem]] + + limit : typing.Optional[float] + + offset : typing.Optional[float] + + sort_by : typing.Optional[TracesSearchRequestSortBy] + + sort_order : typing.Optional[TracesSearchRequestSortOrder] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TracesSearchResponse + Success + + Examples + -------- + import asyncio + + from mirascope.api._generated import AsyncMirascope + + client = AsyncMirascope() + + + async def main() -> None: + await client.traces.search( + start_time="startTime", + end_time="endTime", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.search( + start_time=start_time, + end_time=end_time, + query=query, + trace_id=trace_id, + span_id=span_id, + model=model, + provider=provider, + function_id=function_id, + function_name=function_name, + has_error=has_error, + min_tokens=min_tokens, + max_tokens=max_tokens, + min_duration=min_duration, + max_duration=max_duration, + attribute_filters=attribute_filters, + limit=limit, + offset=offset, + sort_by=sort_by, + sort_order=sort_order, + request_options=request_options, + ) + return _response.data + + async def gettracedetail( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TracesGetTraceDetailResponse: + """ + Parameters + ---------- + trace_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TracesGetTraceDetailResponse + Success + + Examples + -------- + import asyncio + + from mirascope.api._generated import AsyncMirascope + + client = AsyncMirascope() + + + async def main() -> None: + await client.traces.gettracedetail( + trace_id="traceId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.gettracedetail( + trace_id, request_options=request_options + ) + return _response.data + + async def getanalyticssummary( + self, + *, + start_time: str, + end_time: str, + function_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TracesGetAnalyticsSummaryResponse: + """ + Parameters + ---------- + start_time : str + + end_time : str + + function_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TracesGetAnalyticsSummaryResponse + Success + + Examples + -------- + import asyncio + + from mirascope.api._generated import AsyncMirascope + + client = AsyncMirascope() + + + async def main() -> None: + await client.traces.getanalyticssummary( + start_time="startTime", + end_time="endTime", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.getanalyticssummary( + start_time=start_time, + end_time=end_time, + function_id=function_id, + request_options=request_options, + ) return _response.data diff --git a/python/mirascope/api/_generated/traces/raw_client.py b/python/mirascope/api/_generated/traces/raw_client.py index 2686e59f70..8b29b9fc92 100644 --- a/python/mirascope/api/_generated/traces/raw_client.py +++ b/python/mirascope/api/_generated/traces/raw_client.py @@ -6,6 +6,7 @@ from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from ..core.serialization import convert_and_respect_annotation_metadata @@ -20,8 +21,20 @@ from ..types.not_found_error_body import NotFoundErrorBody from ..types.permission_denied_error import PermissionDeniedError from ..types.unauthorized_error_body import UnauthorizedErrorBody -from .types.traces_create_request_resource_spans_item import TracesCreateRequestResourceSpansItem +from .types.traces_create_request_resource_spans_item import ( + TracesCreateRequestResourceSpansItem, +) from .types.traces_create_response import TracesCreateResponse +from .types.traces_get_analytics_summary_response import ( + TracesGetAnalyticsSummaryResponse, +) +from .types.traces_get_trace_detail_response import TracesGetTraceDetailResponse +from .types.traces_search_request_attribute_filters_item import ( + TracesSearchRequestAttributeFiltersItem, +) +from .types.traces_search_request_sort_by import TracesSearchRequestSortBy +from .types.traces_search_request_sort_order import TracesSearchRequestSortOrder +from .types.traces_search_response import TracesSearchResponse # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -144,42 +157,625 @@ def create( ) _response_json = _response.json() except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def search( + self, + *, + start_time: str, + end_time: str, + query: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + span_id: typing.Optional[str] = OMIT, + model: typing.Optional[typing.Sequence[str]] = OMIT, + provider: typing.Optional[typing.Sequence[str]] = OMIT, + function_id: typing.Optional[str] = OMIT, + function_name: typing.Optional[str] = OMIT, + has_error: typing.Optional[bool] = OMIT, + min_tokens: typing.Optional[float] = OMIT, + max_tokens: typing.Optional[float] = OMIT, + min_duration: typing.Optional[float] = OMIT, + max_duration: typing.Optional[float] = OMIT, + attribute_filters: typing.Optional[ + typing.Sequence[TracesSearchRequestAttributeFiltersItem] + ] = OMIT, + limit: typing.Optional[float] = OMIT, + offset: typing.Optional[float] = OMIT, + sort_by: typing.Optional[TracesSearchRequestSortBy] = OMIT, + sort_order: typing.Optional[TracesSearchRequestSortOrder] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TracesSearchResponse]: + """ + Parameters + ---------- + start_time : str + + end_time : str + + query : typing.Optional[str] + + trace_id : typing.Optional[str] + + span_id : typing.Optional[str] + + model : typing.Optional[typing.Sequence[str]] + + provider : typing.Optional[typing.Sequence[str]] + + function_id : typing.Optional[str] + + function_name : typing.Optional[str] + + has_error : typing.Optional[bool] + + min_tokens : typing.Optional[float] + + max_tokens : typing.Optional[float] + + min_duration : typing.Optional[float] + + max_duration : typing.Optional[float] + + attribute_filters : typing.Optional[typing.Sequence[TracesSearchRequestAttributeFiltersItem]] + + limit : typing.Optional[float] + + offset : typing.Optional[float] + + sort_by : typing.Optional[TracesSearchRequestSortBy] + + sort_order : typing.Optional[TracesSearchRequestSortOrder] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TracesSearchResponse] + Success + """ + _response = self._client_wrapper.httpx_client.request( + "traces/search", + method="POST", + json={ + "startTime": start_time, + "endTime": end_time, + "query": query, + "traceId": trace_id, + "spanId": span_id, + "model": model, + "provider": provider, + "functionId": function_id, + "functionName": function_name, + "hasError": has_error, + "minTokens": min_tokens, + "maxTokens": max_tokens, + "minDuration": min_duration, + "maxDuration": max_duration, + "attributeFilters": convert_and_respect_annotation_metadata( + object_=attribute_filters, + annotation=typing.Sequence[TracesSearchRequestAttributeFiltersItem], + direction="write", + ), + "limit": limit, + "offset": offset, + "sortBy": sort_by, + "sortOrder": sort_order, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TracesSearchResponse, + parse_obj_as( + type_=TracesSearchResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + HttpApiDecodeError, + parse_obj_as( + type_=HttpApiDecodeError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedErrorBody, + parse_obj_as( + type_=UnauthorizedErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + PermissionDeniedError, + parse_obj_as( + type_=PermissionDeniedError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def gettracedetail( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TracesGetTraceDetailResponse]: + """ + Parameters + ---------- + trace_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TracesGetTraceDetailResponse] + Success + """ + _response = self._client_wrapper.httpx_client.request( + f"traces/{jsonable_encoder(trace_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TracesGetTraceDetailResponse, + parse_obj_as( + type_=TracesGetTraceDetailResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + HttpApiDecodeError, + parse_obj_as( + type_=HttpApiDecodeError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedErrorBody, + parse_obj_as( + type_=UnauthorizedErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + PermissionDeniedError, + parse_obj_as( + type_=PermissionDeniedError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + NotFoundErrorBody, + parse_obj_as( + type_=NotFoundErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def getanalyticssummary( + self, + *, + start_time: str, + end_time: str, + function_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TracesGetAnalyticsSummaryResponse]: + """ + Parameters + ---------- + start_time : str + + end_time : str + + function_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TracesGetAnalyticsSummaryResponse] + Success + """ + _response = self._client_wrapper.httpx_client.request( + "traces/analytics", + method="GET", + params={ + "startTime": start_time, + "endTime": end_time, + "functionId": function_id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TracesGetAnalyticsSummaryResponse, + parse_obj_as( + type_=TracesGetAnalyticsSummaryResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + HttpApiDecodeError, + parse_obj_as( + type_=HttpApiDecodeError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedErrorBody, + parse_obj_as( + type_=UnauthorizedErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + PermissionDeniedError, + parse_obj_as( + type_=PermissionDeniedError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) class AsyncRawTracesClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): self._client_wrapper = client_wrapper - async def create( + async def create( + self, + *, + resource_spans: typing.Sequence[TracesCreateRequestResourceSpansItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TracesCreateResponse]: + """ + Parameters + ---------- + resource_spans : typing.Sequence[TracesCreateRequestResourceSpansItem] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TracesCreateResponse] + Success + """ + _response = await self._client_wrapper.httpx_client.request( + "traces", + method="POST", + json={ + "resourceSpans": convert_and_respect_annotation_metadata( + object_=resource_spans, + annotation=typing.Sequence[TracesCreateRequestResourceSpansItem], + direction="write", + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TracesCreateResponse, + parse_obj_as( + type_=TracesCreateResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + HttpApiDecodeError, + parse_obj_as( + type_=HttpApiDecodeError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedErrorBody, + parse_obj_as( + type_=UnauthorizedErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + PermissionDeniedError, + parse_obj_as( + type_=PermissionDeniedError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + NotFoundErrorBody, + parse_obj_as( + type_=NotFoundErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + AlreadyExistsError, + parse_obj_as( + type_=AlreadyExistsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def search( self, *, - resource_spans: typing.Sequence[TracesCreateRequestResourceSpansItem], + start_time: str, + end_time: str, + query: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + span_id: typing.Optional[str] = OMIT, + model: typing.Optional[typing.Sequence[str]] = OMIT, + provider: typing.Optional[typing.Sequence[str]] = OMIT, + function_id: typing.Optional[str] = OMIT, + function_name: typing.Optional[str] = OMIT, + has_error: typing.Optional[bool] = OMIT, + min_tokens: typing.Optional[float] = OMIT, + max_tokens: typing.Optional[float] = OMIT, + min_duration: typing.Optional[float] = OMIT, + max_duration: typing.Optional[float] = OMIT, + attribute_filters: typing.Optional[ + typing.Sequence[TracesSearchRequestAttributeFiltersItem] + ] = OMIT, + limit: typing.Optional[float] = OMIT, + offset: typing.Optional[float] = OMIT, + sort_by: typing.Optional[TracesSearchRequestSortBy] = OMIT, + sort_order: typing.Optional[TracesSearchRequestSortOrder] = OMIT, request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[TracesCreateResponse]: + ) -> AsyncHttpResponse[TracesSearchResponse]: """ Parameters ---------- - resource_spans : typing.Sequence[TracesCreateRequestResourceSpansItem] + start_time : str + + end_time : str + + query : typing.Optional[str] + + trace_id : typing.Optional[str] + + span_id : typing.Optional[str] + + model : typing.Optional[typing.Sequence[str]] + + provider : typing.Optional[typing.Sequence[str]] + + function_id : typing.Optional[str] + + function_name : typing.Optional[str] + + has_error : typing.Optional[bool] + + min_tokens : typing.Optional[float] + + max_tokens : typing.Optional[float] + + min_duration : typing.Optional[float] + + max_duration : typing.Optional[float] + + attribute_filters : typing.Optional[typing.Sequence[TracesSearchRequestAttributeFiltersItem]] + + limit : typing.Optional[float] + + offset : typing.Optional[float] + + sort_by : typing.Optional[TracesSearchRequestSortBy] + + sort_order : typing.Optional[TracesSearchRequestSortOrder] request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[TracesCreateResponse] + AsyncHttpResponse[TracesSearchResponse] Success """ _response = await self._client_wrapper.httpx_client.request( - "traces", + "traces/search", method="POST", json={ - "resourceSpans": convert_and_respect_annotation_metadata( - object_=resource_spans, - annotation=typing.Sequence[TracesCreateRequestResourceSpansItem], + "startTime": start_time, + "endTime": end_time, + "query": query, + "traceId": trace_id, + "spanId": span_id, + "model": model, + "provider": provider, + "functionId": function_id, + "functionName": function_name, + "hasError": has_error, + "minTokens": min_tokens, + "maxTokens": max_tokens, + "minDuration": min_duration, + "maxDuration": max_duration, + "attributeFilters": convert_and_respect_annotation_metadata( + object_=attribute_filters, + annotation=typing.Sequence[TracesSearchRequestAttributeFiltersItem], direction="write", ), + "limit": limit, + "offset": offset, + "sortBy": sort_by, + "sortOrder": sort_order, }, headers={ "content-type": "application/json", @@ -190,9 +786,97 @@ async def create( try: if 200 <= _response.status_code < 300: _data = typing.cast( - TracesCreateResponse, + TracesSearchResponse, parse_obj_as( - type_=TracesCreateResponse, # type: ignore + type_=TracesSearchResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + HttpApiDecodeError, + parse_obj_as( + type_=HttpApiDecodeError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedErrorBody, + parse_obj_as( + type_=UnauthorizedErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + PermissionDeniedError, + parse_obj_as( + type_=PermissionDeniedError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def gettracedetail( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TracesGetTraceDetailResponse]: + """ + Parameters + ---------- + trace_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TracesGetTraceDetailResponse] + Success + """ + _response = await self._client_wrapper.httpx_client.request( + f"traces/{jsonable_encoder(trace_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TracesGetTraceDetailResponse, + parse_obj_as( + type_=TracesGetTraceDetailResponse, # type: ignore object_=_response.json(), ), ) @@ -241,13 +925,104 @@ async def create( ), ), ) - if _response.status_code == 409: - raise ConflictError( + if _response.status_code == 500: + raise InternalServerError( headers=dict(_response.headers), body=typing.cast( - AlreadyExistsError, + typing.Optional[typing.Any], parse_obj_as( - type_=AlreadyExistsError, # type: ignore + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def getanalyticssummary( + self, + *, + start_time: str, + end_time: str, + function_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TracesGetAnalyticsSummaryResponse]: + """ + Parameters + ---------- + start_time : str + + end_time : str + + function_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TracesGetAnalyticsSummaryResponse] + Success + """ + _response = await self._client_wrapper.httpx_client.request( + "traces/analytics", + method="GET", + params={ + "startTime": start_time, + "endTime": end_time, + "functionId": function_id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TracesGetAnalyticsSummaryResponse, + parse_obj_as( + type_=TracesGetAnalyticsSummaryResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + HttpApiDecodeError, + parse_obj_as( + type_=HttpApiDecodeError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedErrorBody, + parse_obj_as( + type_=UnauthorizedErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + PermissionDeniedError, + parse_obj_as( + type_=PermissionDeniedError, # type: ignore object_=_response.json(), ), ), @@ -265,5 +1040,13 @@ async def create( ) _response_json = _response.json() except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/python/mirascope/api/_generated/traces/types/__init__.py b/python/mirascope/api/_generated/traces/types/__init__.py index 3f6094dfa0..8437ea6632 100644 --- a/python/mirascope/api/_generated/traces/types/__init__.py +++ b/python/mirascope/api/_generated/traces/types/__init__.py @@ -2,8 +2,12 @@ # isort: skip_file -from .traces_create_request_resource_spans_item import TracesCreateRequestResourceSpansItem -from .traces_create_request_resource_spans_item_resource import TracesCreateRequestResourceSpansItemResource +from .traces_create_request_resource_spans_item import ( + TracesCreateRequestResourceSpansItem, +) +from .traces_create_request_resource_spans_item_resource import ( + TracesCreateRequestResourceSpansItemResource, +) from .traces_create_request_resource_spans_item_resource_attributes_item import ( TracesCreateRequestResourceSpansItemResourceAttributesItem, ) @@ -63,6 +67,27 @@ ) from .traces_create_response import TracesCreateResponse from .traces_create_response_partial_success import TracesCreateResponsePartialSuccess +from .traces_get_analytics_summary_response import TracesGetAnalyticsSummaryResponse +from .traces_get_analytics_summary_response_top_functions_item import ( + TracesGetAnalyticsSummaryResponseTopFunctionsItem, +) +from .traces_get_analytics_summary_response_top_models_item import ( + TracesGetAnalyticsSummaryResponseTopModelsItem, +) +from .traces_get_trace_detail_response import TracesGetTraceDetailResponse +from .traces_get_trace_detail_response_spans_item import ( + TracesGetTraceDetailResponseSpansItem, +) +from .traces_search_request_attribute_filters_item import ( + TracesSearchRequestAttributeFiltersItem, +) +from .traces_search_request_attribute_filters_item_operator import ( + TracesSearchRequestAttributeFiltersItemOperator, +) +from .traces_search_request_sort_by import TracesSearchRequestSortBy +from .traces_search_request_sort_order import TracesSearchRequestSortOrder +from .traces_search_response import TracesSearchResponse +from .traces_search_response_spans_item import TracesSearchResponseSpansItem __all__ = [ "TracesCreateRequestResourceSpansItem", @@ -88,4 +113,15 @@ "TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus", "TracesCreateResponse", "TracesCreateResponsePartialSuccess", + "TracesGetAnalyticsSummaryResponse", + "TracesGetAnalyticsSummaryResponseTopFunctionsItem", + "TracesGetAnalyticsSummaryResponseTopModelsItem", + "TracesGetTraceDetailResponse", + "TracesGetTraceDetailResponseSpansItem", + "TracesSearchRequestAttributeFiltersItem", + "TracesSearchRequestAttributeFiltersItemOperator", + "TracesSearchRequestSortBy", + "TracesSearchRequestSortOrder", + "TracesSearchResponse", + "TracesSearchResponseSpansItem", ] diff --git a/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py b/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py new file mode 100644 index 0000000000..ebbaf1c89f --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py @@ -0,0 +1,54 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ...core.serialization import FieldMetadata +from .traces_get_analytics_summary_response_top_functions_item import ( + TracesGetAnalyticsSummaryResponseTopFunctionsItem, +) +from .traces_get_analytics_summary_response_top_models_item import ( + TracesGetAnalyticsSummaryResponseTopModelsItem, +) + + +class TracesGetAnalyticsSummaryResponse(UniversalBaseModel): + total_spans: typing_extensions.Annotated[float, FieldMetadata(alias="totalSpans")] + avg_duration_ms: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="avgDurationMs") + ] = None + p50duration_ms: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="p50DurationMs") + ] = None + p95duration_ms: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="p95DurationMs") + ] = None + p99duration_ms: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="p99DurationMs") + ] = None + error_rate: typing_extensions.Annotated[float, FieldMetadata(alias="errorRate")] + total_tokens: typing_extensions.Annotated[float, FieldMetadata(alias="totalTokens")] + total_cost_usd: typing_extensions.Annotated[ + float, FieldMetadata(alias="totalCostUsd") + ] + top_models: typing_extensions.Annotated[ + typing.List[TracesGetAnalyticsSummaryResponseTopModelsItem], + FieldMetadata(alias="topModels"), + ] + top_functions: typing_extensions.Annotated[ + typing.List[TracesGetAnalyticsSummaryResponseTopFunctionsItem], + FieldMetadata(alias="topFunctions"), + ] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_functions_item.py b/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_functions_item.py new file mode 100644 index 0000000000..f8748ed3d3 --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_functions_item.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class TracesGetAnalyticsSummaryResponseTopFunctionsItem(UniversalBaseModel): + function_name: typing_extensions.Annotated[str, FieldMetadata(alias="functionName")] + count: float + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_models_item.py b/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_models_item.py new file mode 100644 index 0000000000..256473ee87 --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_models_item.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class TracesGetAnalyticsSummaryResponseTopModelsItem(UniversalBaseModel): + model: str + count: float + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/traces/types/traces_get_trace_detail_response.py b/python/mirascope/api/_generated/traces/types/traces_get_trace_detail_response.py new file mode 100644 index 0000000000..5e0bd369c6 --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_get_trace_detail_response.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ...core.serialization import FieldMetadata +from .traces_get_trace_detail_response_spans_item import ( + TracesGetTraceDetailResponseSpansItem, +) + + +class TracesGetTraceDetailResponse(UniversalBaseModel): + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + spans: typing.List[TracesGetTraceDetailResponseSpansItem] + root_span_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="rootSpanId") + ] = None + total_duration_ms: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="totalDurationMs") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py b/python/mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py new file mode 100644 index 0000000000..2eb8fd325d --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py @@ -0,0 +1,90 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class TracesGetTraceDetailResponseSpansItem(UniversalBaseModel): + id: str + trace_db_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceDbId")] + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + span_id: typing_extensions.Annotated[str, FieldMetadata(alias="spanId")] + parent_span_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="parentSpanId") + ] = None + environment_id: typing_extensions.Annotated[ + str, FieldMetadata(alias="environmentId") + ] + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + organization_id: typing_extensions.Annotated[ + str, FieldMetadata(alias="organizationId") + ] + start_time: typing_extensions.Annotated[str, FieldMetadata(alias="startTime")] + end_time: typing_extensions.Annotated[str, FieldMetadata(alias="endTime")] + duration_ms: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="durationMs") + ] = None + name: str + kind: float + status_code: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="statusCode") + ] = None + status_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="statusMessage") + ] = None + model: typing.Optional[str] = None + provider: typing.Optional[str] = None + input_tokens: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="inputTokens") + ] = None + output_tokens: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="outputTokens") + ] = None + total_tokens: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="totalTokens") + ] = None + cost_usd: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="costUsd") + ] = None + function_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="functionId") + ] = None + function_name: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="functionName") + ] = None + function_version: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="functionVersion") + ] = None + error_type: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="errorType") + ] = None + error_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="errorMessage") + ] = None + attributes: str + events: typing.Optional[str] = None + links: typing.Optional[str] = None + service_name: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="serviceName") + ] = None + service_version: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="serviceVersion") + ] = None + resource_attributes: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="resourceAttributes") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item.py b/python/mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item.py new file mode 100644 index 0000000000..b211de88e9 --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .traces_search_request_attribute_filters_item_operator import ( + TracesSearchRequestAttributeFiltersItemOperator, +) + + +class TracesSearchRequestAttributeFiltersItem(UniversalBaseModel): + key: str + operator: TracesSearchRequestAttributeFiltersItemOperator + value: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item_operator.py b/python/mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item_operator.py new file mode 100644 index 0000000000..479017055a --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item_operator.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TracesSearchRequestAttributeFiltersItemOperator = typing.Union[ + typing.Literal["eq", "neq", "contains", "exists"], typing.Any +] diff --git a/python/mirascope/api/_generated/traces/types/traces_search_request_sort_by.py b/python/mirascope/api/_generated/traces/types/traces_search_request_sort_by.py new file mode 100644 index 0000000000..68070f39bc --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_search_request_sort_by.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TracesSearchRequestSortBy = typing.Union[ + typing.Literal["start_time", "duration_ms", "total_tokens"], typing.Any +] diff --git a/python/mirascope/api/_generated/traces/types/traces_search_request_sort_order.py b/python/mirascope/api/_generated/traces/types/traces_search_request_sort_order.py new file mode 100644 index 0000000000..c5f9e18827 --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_search_request_sort_order.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TracesSearchRequestSortOrder = typing.Union[typing.Literal["asc", "desc"], typing.Any] diff --git a/python/mirascope/api/_generated/traces/types/traces_search_response.py b/python/mirascope/api/_generated/traces/types/traces_search_response.py new file mode 100644 index 0000000000..b3639ab46a --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_search_response.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ...core.serialization import FieldMetadata +from .traces_search_response_spans_item import TracesSearchResponseSpansItem + + +class TracesSearchResponse(UniversalBaseModel): + spans: typing.List[TracesSearchResponseSpansItem] + total: float + has_more: typing_extensions.Annotated[bool, FieldMetadata(alias="hasMore")] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/traces/types/traces_search_response_spans_item.py b/python/mirascope/api/_generated/traces/types/traces_search_response_spans_item.py new file mode 100644 index 0000000000..02f99f1cb9 --- /dev/null +++ b/python/mirascope/api/_generated/traces/types/traces_search_response_spans_item.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class TracesSearchResponseSpansItem(UniversalBaseModel): + id: str + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + span_id: typing_extensions.Annotated[str, FieldMetadata(alias="spanId")] + name: str + start_time: typing_extensions.Annotated[str, FieldMetadata(alias="startTime")] + duration_ms: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="durationMs") + ] = None + model: typing.Optional[str] = None + provider: typing.Optional[str] = None + total_tokens: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="totalTokens") + ] = None + function_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="functionId") + ] = None + function_name: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="functionName") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/types/__init__.py b/python/mirascope/api/_generated/types/__init__.py index 3a2ceffee3..ee8e6de3cb 100644 --- a/python/mirascope/api/_generated/types/__init__.py +++ b/python/mirascope/api/_generated/types/__init__.py @@ -4,6 +4,7 @@ from .already_exists_error import AlreadyExistsError from .already_exists_error_tag import AlreadyExistsErrorTag +from .click_house_error import ClickHouseError from .database_error import DatabaseError from .database_error_tag import DatabaseErrorTag from .http_api_decode_error import HttpApiDecodeError @@ -30,6 +31,7 @@ __all__ = [ "AlreadyExistsError", "AlreadyExistsErrorTag", + "ClickHouseError", "DatabaseError", "DatabaseErrorTag", "HttpApiDecodeError", diff --git a/python/mirascope/api/_generated/types/click_house_error.py b/python/mirascope/api/_generated/types/click_house_error.py new file mode 100644 index 0000000000..2791a309fc --- /dev/null +++ b/python/mirascope/api/_generated/types/click_house_error.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class ClickHouseError(UniversalBaseModel): + message: str + cause: typing.Optional[typing.Optional[typing.Any]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/mirascope/api/_generated/types/internal_server_error_body.py b/python/mirascope/api/_generated/types/internal_server_error_body.py index 92969a4fa3..b006426ef5 100644 --- a/python/mirascope/api/_generated/types/internal_server_error_body.py +++ b/python/mirascope/api/_generated/types/internal_server_error_body.py @@ -15,7 +15,9 @@ class InternalServerErrorBody_StripeError(UniversalBaseModel): cause: typing.Optional[typing.Optional[typing.Any]] = None if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 else: class Config: @@ -31,7 +33,9 @@ class InternalServerErrorBody_DatabaseError(UniversalBaseModel): tag: DatabaseErrorTag if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) # type: ignore # Pydantic v2 else: class Config: @@ -40,4 +44,6 @@ class Config: extra = pydantic.Extra.allow -InternalServerErrorBody = typing.Union[InternalServerErrorBody_StripeError, InternalServerErrorBody_DatabaseError] +InternalServerErrorBody = typing.Union[ + InternalServerErrorBody_StripeError, InternalServerErrorBody_DatabaseError +]