From 3962501198dc13bb22110c3c78fbdd1b2211dda1 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Mon, 15 Sep 2025 15:45:02 +0200 Subject: [PATCH 1/3] refactor: migrate Davis CoPilot integration to official SDK - Replace manual API calls with @dynatrace-sdk/client-davis-copilot - Simplify imports by removing unused type exports - Keep only Dql2NlResponse export for external compatibility - Improve reliability and maintainability of Davis CoPilot functions - Update function implementations to use PublicClient from official SDK --- CHANGELOG.md | 1 + package-lock.json | 9 ++ package.json | 1 + src/capabilities/davis-copilot.ts | 246 ++++++++---------------------- 4 files changed, 73 insertions(+), 184 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7dafd..cd91254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Fixed an issue with stateless HTTP server only taking a single connection - Added Grail budget tracking with `DT_GRAIL_QUERY_BUDGET_GB` environment variable (default: 1000 GB, setting it to `-1` disables it), as well as warnings and exceeded alerts in `execute_dql` tool responses - Enforce Grail budget by throwing an exception when the budget has been exceeded, preventing further DQL query execution +- Refactored Davis CoPilot integration to use official `@dynatrace-sdk/client-davis-copilot` package instead of manual API calls, improving reliability and maintainability ## 0.6.0 (Release Candidate 1) diff --git a/package-lock.json b/package-lock.json index 9259b33..16ec085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@dynatrace-sdk/client-automation": "^5.3.0", + "@dynatrace-sdk/client-davis-copilot": "^1.0.0", "@dynatrace-sdk/client-platform-management-service": "^1.6.3", "@dynatrace-sdk/client-query": "^1.18.1", "@dynatrace-sdk/shared-errors": "^1.0.0", @@ -703,6 +704,14 @@ "@dynatrace-sdk/shared-errors": "^1.0.0" } }, + "node_modules/@dynatrace-sdk/client-davis-copilot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@dynatrace-sdk/client-davis-copilot/-/client-davis-copilot-1.0.0.tgz", + "integrity": "sha512-foYWvcoI3kp4sWpxxKEk/pC32nONgWalzshU/0lgdgoTYZbKC7nrW4LNnj2Q31q3UhB6d7HZlkThuq3bnv70ig==", + "dependencies": { + "@dynatrace-sdk/http-client": "^1.3.1" + } + }, "node_modules/@dynatrace-sdk/client-platform-management-service": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@dynatrace-sdk/client-platform-management-service/-/client-platform-management-service-1.7.0.tgz", diff --git a/package.json b/package.json index 3f2b9e1..dfb80bb 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "license": "MIT", "dependencies": { "@dynatrace-sdk/client-automation": "^5.3.0", + "@dynatrace-sdk/client-davis-copilot": "^1.0.0", "@dynatrace-sdk/client-platform-management-service": "^1.6.3", "@dynatrace-sdk/client-query": "^1.18.1", "@dynatrace-sdk/shared-errors": "^1.0.0", diff --git a/src/capabilities/davis-copilot.ts b/src/capabilities/davis-copilot.ts index a80f08f..e0ae4fc 100644 --- a/src/capabilities/davis-copilot.ts +++ b/src/capabilities/davis-copilot.ts @@ -1,11 +1,6 @@ -import { HttpClient } from '@dynatrace-sdk/http-client'; - /** * Davis CoPilot API Integration * - * !!! Note: Once @dynatrace-sdk/client-davis-copilot is available, we need to refactor this file. - * !!! Disclaimer: Those API Calls might break any time, as they are not yet part of the official Dynatrace SDK. - * * This module provides access to Davis CoPilot AI capabilities including: * - Natural Language to DQL conversion * - DQL explanation in plain English @@ -20,148 +15,18 @@ import { HttpClient } from '@dynatrace-sdk/http-client'; * in Dynatrace, including problem events, security issues, logs, metrics, and spans. */ -// TypeScript interfaces based on OpenAPI spec -// ToDo: Once @dynatrace-sdk/client-davis-copilot is available, we need to refactor this file. -export type Status = 'SUCCESSFUL' | 'SUCCESSFUL_WITH_WARNINGS' | 'FAILED'; - -export interface Nl2DqlRequest { - text: string; -} - -export interface Nl2DqlResponse { - dql: string; - messageToken: string; - status: Status; - metadata?: Metadata; -} - -export interface Dql2NlRequest { - dql: string; -} - -export interface Dql2NlResponse { - summary: string; - explanation: string; - messageToken: string; - status: Status; - metadata?: Metadata; -} - -export interface ConversationRequest { - text: string; - context?: ConversationContext[]; - annotations?: Record; - state?: State; -} - -export interface ConversationResponse { - text: string; - messageToken: string; - state: State; - metadata: MetadataWithSource; - status: Status; -} - -export interface ConversationContext { - type: 'supplementary' | 'document-retrieval' | 'instruction'; - value: string; -} - -export interface State { - version?: string; - conversationId?: string; - skillName?: string; - history?: Array<{ - role: string; - text: string; - supplementary?: string | null; - }>; -} - -export interface Metadata { - notifications?: Notification[]; -} - -export interface MetadataWithSource extends Metadata { - sources?: SourceDocument[]; -} - -export interface Notification { - severity?: string; - notificationType?: string; - message?: string; -} - -export interface SourceDocument { - title?: string; - url?: string; - type?: string; -} - -export interface Nl2DqlFeedbackRequest { - messageToken: string; - origin: string; - feedback: Nl2DqlFeedback; - userQuery: string; - queryExplanation?: string; - generatedDql?: string; -} - -export interface Nl2DqlFeedback { - type: 'positive' | 'negative'; - text?: string; - category?: string; - improvement?: Nl2DqlImprovedSummary; -} - -export interface Nl2DqlImprovedSummary { - text: string; - confirmation: boolean; -} - -export interface Dql2NlFeedbackRequest { - messageToken: string; - origin: string; - feedback: Dql2NlFeedback; - userQuery: string; - queryExplanation?: string; - generatedDql?: string; -} - -export interface Dql2NlFeedback { - type: 'positive' | 'negative'; - text?: string; - category?: string; - improvement?: Dql2NlImprovedSummary; -} - -export interface Dql2NlImprovedSummary { - text: string; - confirmation: boolean; -} - -export interface ConversationFeedbackRequest { - messageToken: string; - origin: string; - feedback: ConversationFeedback; - userPrompt?: string; - copilotResponse?: string; - sources?: string[]; -} - -export interface ConversationFeedback { - type: 'positive' | 'negative'; - text?: string; - category?: string; - improvement?: ConversationImprovedSummary; -} - -export interface ConversationImprovedSummary { - text: string; - confirmation: boolean; -} - -// API Functions +import { HttpClient } from '@dynatrace-sdk/http-client'; +import { + PublicClient, + Nl2DqlResponse, + Dql2NlResponse, + ConversationResponse, + ConversationContext, + State, +} from '@dynatrace-sdk/client-davis-copilot'; + +// Re-export types that are used externally +export type { Dql2NlResponse }; /** * Generate DQL from natural language @@ -170,19 +35,11 @@ export interface ConversationImprovedSummary { * security issues, logs, metrics, spans, and custom data. */ export const generateDqlFromNaturalLanguage = async (dtClient: HttpClient, text: string): Promise => { - const request: Nl2DqlRequest = { text }; + const client = new PublicClient(dtClient); - const response = await dtClient.send({ - method: 'POST', - url: '/platform/davis/copilot/v0.2/skills/nl2dql:generate', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(request), + return await client.nl2dql({ + body: { text }, }); - - return await response.body('json'); }; /** @@ -192,19 +49,11 @@ export const generateDqlFromNaturalLanguage = async (dtClient: HttpClient, text: * queries for problem events, security issues, and performance metrics. */ export const explainDqlInNaturalLanguage = async (dtClient: HttpClient, dql: string): Promise => { - const request: Dql2NlRequest = { dql }; + const client = new PublicClient(dtClient); - const response = await dtClient.send({ - method: 'POST', - url: '/platform/davis/copilot/v0.2/skills/dql2nl:explain', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: request, // Not sure why this does not need JSON.stringify, but it only works like this; once we have the SDK, this will be consistent + return await client.dql2nl({ + body: { dql }, }); - - return await response.body('json'); }; export const chatWithDavisCopilot = async ( @@ -214,22 +63,51 @@ export const chatWithDavisCopilot = async ( annotations?: Record, state?: State, ): Promise => { - const request: ConversationRequest = { - text, - context, - annotations, - state, - }; - - const response = await dtClient.send({ - method: 'POST', - url: '/platform/davis/copilot/v0.2/skills/conversations:message', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', + const client = new PublicClient(dtClient); + + const response = await client.recommenderConversation({ + body: { + text, + context, + annotations, + state, }, - body: JSON.stringify(request), }); - return await response.body('json'); + // Handle both streaming and non-streaming responses + // If it's an array (streaming), we need to extract the final response + // If it's already a ConversationResponse, return it directly + if (Array.isArray(response)) { + // For streaming responses, find the end event with the final response + const endEvent = response.find((event) => event.event === 'end') as any; + if (endEvent?.data) { + return { + text: endEvent.data.answer || '', + messageToken: '', // Will need to get from start event + status: 'SUCCESSFUL', + state: endEvent.data.state || {}, + metadata: { + sources: endEvent.data.sources || [], + }, + } as ConversationResponse; + } + + // Fallback: try to construct response from available events + const startEvent = response.find((event) => event.event === 'start') as any; + const messageToken = startEvent?.data?.messageToken || ''; + + const contentEvents = response.filter((event) => event.event === 'content'); + const text = contentEvents.map((event: any) => event.data?.text || '').join(''); + + return { + text, + messageToken, + status: 'SUCCESSFUL', + state: {}, + metadata: { sources: [] }, + } as ConversationResponse; + } + + // Direct ConversationResponse + return response as ConversationResponse; }; From 2134360f5fda85287975b019b25e3b661bf79c9f Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Tue, 16 Sep 2025 14:29:10 +0200 Subject: [PATCH 2/3] chore: Remove redundant 'as type' statement for copilot sdk --- src/capabilities/davis-copilot.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/capabilities/davis-copilot.ts b/src/capabilities/davis-copilot.ts index e0ae4fc..173e0e2 100644 --- a/src/capabilities/davis-copilot.ts +++ b/src/capabilities/davis-copilot.ts @@ -89,7 +89,7 @@ export const chatWithDavisCopilot = async ( metadata: { sources: endEvent.data.sources || [], }, - } as ConversationResponse; + }; } // Fallback: try to construct response from available events @@ -105,9 +105,9 @@ export const chatWithDavisCopilot = async ( status: 'SUCCESSFUL', state: {}, metadata: { sources: [] }, - } as ConversationResponse; + }; } // Direct ConversationResponse - return response as ConversationResponse; + return response; }; From 9b45112c30044c85b1a48b1fcdad805288e7a6b4 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Tue, 16 Sep 2025 15:40:28 +0200 Subject: [PATCH 3/3] chore: Remove streaming response setup for chatWithDavisCopilot --- src/capabilities/davis-copilot.ts | 40 ++++++------------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/src/capabilities/davis-copilot.ts b/src/capabilities/davis-copilot.ts index 173e0e2..595bcb9 100644 --- a/src/capabilities/davis-copilot.ts +++ b/src/capabilities/davis-copilot.ts @@ -23,6 +23,7 @@ import { ConversationResponse, ConversationContext, State, + RecommenderResponse, } from '@dynatrace-sdk/client-davis-copilot'; // Re-export types that are used externally @@ -65,7 +66,7 @@ export const chatWithDavisCopilot = async ( ): Promise => { const client = new PublicClient(dtClient); - const response = await client.recommenderConversation({ + const response: RecommenderResponse = await client.recommenderConversation({ body: { text, context, @@ -74,40 +75,13 @@ export const chatWithDavisCopilot = async ( }, }); - // Handle both streaming and non-streaming responses - // If it's an array (streaming), we need to extract the final response - // If it's already a ConversationResponse, return it directly + // Type guard: RecommenderResponse is ConversationResponse | EventArray + // In practice, the SDK defaults to non-streaming and returns ConversationResponse if (Array.isArray(response)) { - // For streaming responses, find the end event with the final response - const endEvent = response.find((event) => event.event === 'end') as any; - if (endEvent?.data) { - return { - text: endEvent.data.answer || '', - messageToken: '', // Will need to get from start event - status: 'SUCCESSFUL', - state: endEvent.data.state || {}, - metadata: { - sources: endEvent.data.sources || [], - }, - }; - } - - // Fallback: try to construct response from available events - const startEvent = response.find((event) => event.event === 'start') as any; - const messageToken = startEvent?.data?.messageToken || ''; - - const contentEvents = response.filter((event) => event.event === 'content'); - const text = contentEvents.map((event: any) => event.data?.text || '').join(''); - - return { - text, - messageToken, - status: 'SUCCESSFUL', - state: {}, - metadata: { sources: [] }, - }; + throw new Error( + 'Unexpected streaming response format. Please raise an issue at https://github.com/dynatrace-oss/dynatrace-mcp/issues.', + ); } - // Direct ConversationResponse return response; };