Skip to content

Commit 5739346

Browse files
authored
Datasource discovery tool (#59)
* [WIP] Add getDataSourceSuggestions tool * make sure datasource discovery tool is invoked only if cluster version is > 10.13.0.cl * address comments * add test for number of tools returned
1 parent 838cac5 commit 5739346

File tree

12 files changed

+1286
-32
lines changed

12 files changed

+1286
-32
lines changed

src/servers/mcp-server-base.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ export abstract class BaseMCPServer extends Server {
6363
});
6464
}
6565

66+
/**
67+
* Check if data source discovery is available
68+
*/
69+
protected isDatasourceDiscoveryAvailable(): boolean {
70+
const enableSpotterDataSourceDiscovery = this.sessionInfo?.enableSpotterDataSourceDiscovery;
71+
return !!enableSpotterDataSourceDiscovery;
72+
};
73+
6674
/**
6775
* Initialize span with common attributes (user_guid and instance_url)
6876
*/

src/servers/mcp-server.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,17 @@ const CreateLiveboardSchema = z.object({
6666

6767
});
6868

69+
const GetDataSourceSuggestionsSchema = z.object({
70+
query: z.string().describe(`The query to get data source suggestions for, this could be a high level task or question the user is asking or hoping to get answered.
71+
There can be multiple data sources. Each data source can be used to get the data for the user's query using the other tools getRelevantQuestions and getAnswer.`),
72+
});
73+
6974
enum ToolName {
7075
Ping = "ping",
7176
GetRelevantQuestions = "getRelevantQuestions",
7277
GetAnswer = "getAnswer",
7378
CreateLiveboard = "createLiveboard",
79+
GetDataSourceSuggestions = "getDataSourceSuggestions",
7480
}
7581

7682
export class MCPServer extends BaseMCPServer {
@@ -120,7 +126,17 @@ export class MCPServer extends BaseMCPServer {
120126
readOnlyHint: true,
121127
destructiveHint: false,
122128
},
123-
}
129+
},
130+
...(this.isDatasourceDiscoveryAvailable() ? [{
131+
name: ToolName.GetDataSourceSuggestions,
132+
description: "Get data source suggestions for a query. Use this tool only if there is not datasource id provided in the context or the users query. If mulitple data sources are returned, and the confidence difference between the top two data sources is less than 0.3, ask the user to select the most relevant data source. Otherwise use the data source with the highest confidence to get the relevant questions and answers for the query.",
133+
inputSchema: zodToJsonSchema(GetDataSourceSuggestionsSchema) as ToolInput,
134+
annotations: {
135+
title: "Get Data Source Suggestions for a Query",
136+
readOnlyHint: true,
137+
destructiveHint: false,
138+
},
139+
}] : []),
124140
]
125141
};
126142
}
@@ -188,6 +204,10 @@ export class MCPServer extends BaseMCPServer {
188204
return this.callCreateLiveboard(request);
189205
}
190206

207+
case ToolName.GetDataSourceSuggestions: {
208+
return this.callGetDataSourceSuggestions(request);
209+
}
210+
191211
default:
192212
throw new Error(`Unknown tool: ${name}`);
193213
}
@@ -255,6 +275,25 @@ Provide this url to the user as a link to view the liveboard in ThoughtSpot.`;
255275
return this.createSuccessResponse(successMessage, "Liveboard created successfully");
256276
}
257277

278+
@WithSpan('call-get-data-source-suggestions')
279+
async callGetDataSourceSuggestions(request: z.infer<typeof CallToolRequestSchema>) {
280+
const { query } = GetDataSourceSuggestionsSchema.parse(request.params.arguments);
281+
const dataSources = await this.getThoughtSpotService().getDataSourceSuggestions(query);
282+
283+
if (!dataSources || dataSources.length === 0) {
284+
return this.createErrorResponse("No data source suggestions found", "No data source suggestions found");
285+
}
286+
287+
// Return information for all suggested data sources
288+
const dataSourcesInfo = dataSources.map(ds => ({
289+
header: ds.header,
290+
confidence: ds.confidence,
291+
llmReasoning: ds.llmReasoning
292+
}));
293+
294+
return this.createSuccessResponse(JSON.stringify(dataSourcesInfo), `${dataSources.length} data source suggestion(s) found`);
295+
}
296+
258297
private _sources: {
259298
list: DataSource[];
260299
map: Map<string, DataSource>;

src/servers/openai-mcp-server.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,25 @@ export class OpenAIDeepResearchMCPServer extends BaseMCPServer {
120120

121121
return this.createStructuredContentSuccessResponse({ results }, "Relevant questions found");
122122
}
123-
124123
// Search for datasources in case the query is not of the form "datasource:<id> <query-with-spaces>"
125-
// TODO: Implement this
126-
return this.createStructuredContentSuccessResponse({ results: [] }, "No relevant questions found");
124+
if (!this.isDatasourceDiscoveryAvailable()) {
125+
return this.createStructuredContentSuccessResponse({ results: [] }, "No relevant questions found");
126+
}
127+
const dataSources = await this.getThoughtSpotService().getDataSourceSuggestions(queryWithoutDatasourceId);
128+
if (!dataSources || dataSources.length === 0) {
129+
return this.createSuccessResponse("No relevant data sources found, please provide a datasource id in the query");
130+
}
131+
const results = dataSources.map(d => ({
132+
id: `datasource:///${d.header.guid}`,
133+
title: d.header.displayName,
134+
text: `Datasource Description: ${d.header.description}. Confidence that this datasource is relevant to the query: ${d.confidence}. Reasoning for the confidence: ${d.llmReasoning}.
135+
Use this datasource to search for relevant questions and to get answers for the questions.
136+
Use the search tool to search for relevant questions with the format "datasource:<id> <query-with-spaces>" and the fetch tool to get answers for the questions.`,
137+
}));
138+
139+
return this.createStructuredContentSuccessResponse({ results }, "Relevant questions found");
140+
141+
127142
}
128143

129144
@WithSpan('call-fetch')

src/thoughtspot/thoughtspot-client.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { RequestContext, ResponseContext } from "@thoughtspot/rest-api-sdk"
33
import YAML from "yaml";
44
import type { Observable } from "rxjs";
55
import { of } from "rxjs";
6-
import type { SessionInfo } from "./types";
6+
import type { SessionInfo, DataSourceSuggestionResponse } from "./types";
77

88
export const getThoughtSpotClient = (instanceUrl: string, bearerToken: string) => {
99
const config = createBearerAuthenticationConfig(
@@ -27,6 +27,7 @@ export const getThoughtSpotClient = (instanceUrl: string, bearerToken: string) =
2727
(client as any).instanceUrl = instanceUrl;
2828
addExportUnsavedAnswerTML(client, instanceUrl, bearerToken);
2929
addGetSessionInfo(client, instanceUrl, bearerToken);
30+
addQueryGetDataSourceSuggestions(client, instanceUrl, bearerToken);
3031
return client;
3132
}
3233

@@ -105,4 +106,50 @@ async function addGetSessionInfo(client: any, instanceUrl: string, token: string
105106
const info = data.info;
106107
return info;
107108
};
109+
}
110+
111+
const getDataSourceSuggestionsQuery = `
112+
query QueryGetDataSourceSuggestions($request: Input_eureka_DataSourceSuggestionRequest) {
113+
queryGetDataSourceSuggestions(request: $request) {
114+
dataSources {
115+
confidence
116+
header {
117+
description
118+
displayName
119+
guid
120+
}
121+
llmReasoning
122+
}
123+
}
124+
}`;
125+
126+
// This is a workaround until we get the public API for this
127+
function addQueryGetDataSourceSuggestions(client: any, instanceUrl: string, token: string) {
128+
(client as any).queryGetDataSourceSuggestions = async (query: string): Promise<DataSourceSuggestionResponse> => {
129+
const endpoint = "/prism/";
130+
const fetchOptions = {
131+
method: "POST",
132+
headers: {
133+
"Content-Type": "application/json",
134+
"Accept": "application/json",
135+
"user-agent": "ThoughtSpot-ts-client",
136+
"Authorization": `Bearer ${token}`,
137+
},
138+
body: JSON.stringify({
139+
query: getDataSourceSuggestionsQuery,
140+
variables: {
141+
request: {
142+
query: query
143+
}
144+
},
145+
operationName: "QueryGetDataSourceSuggestions"
146+
})
147+
};
148+
console.log("fetchOptions", fetchOptions);
149+
const response = await fetch(`${instanceUrl}${endpoint}`, fetchOptions);
150+
console.log("response", response);
151+
const data = await response.json() as any;
152+
console.log("data", data);
153+
return data.data.queryGetDataSourceSuggestions;
154+
};
108155
}

src/thoughtspot/thoughtspot-service.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ThoughtSpotRestApi } from "@thoughtspot/rest-api-sdk";
22
import { SpanStatusCode, trace, context } from "@opentelemetry/api";
33
import { getActiveSpan, WithSpan } from "../metrics/tracing/tracing-utils";
4-
import type { DataSource, SessionInfo } from "./types";
4+
import type { DataSource, SessionInfo, DataSourceSuggestion } from "./types";
55

66

77
/**
@@ -10,6 +10,56 @@ import type { DataSource, SessionInfo } from "./types";
1010
export class ThoughtSpotService {
1111
constructor(private client: ThoughtSpotRestApi) { }
1212

13+
@WithSpan('discover-data-sources')
14+
async discoverDataSources(query?: string): Promise<DataSource[] | DataSourceSuggestion[] | null> {
15+
const span = getActiveSpan();
16+
span?.addEvent("discover-data-sources");
17+
18+
// If a query is provided, use intelligent data source suggestions
19+
if (query) {
20+
span?.addEvent("get-data-source-suggestions");
21+
return await this.getDataSourceSuggestions(query);
22+
}
23+
24+
// Otherwise, fallback to getting all data sources
25+
const dataSources = await this.getDataSources();
26+
return dataSources;
27+
}
28+
29+
/**
30+
* Get intelligent data source suggestions based on a query using GraphQL
31+
*/
32+
@WithSpan('get-data-source-suggestions')
33+
async getDataSourceSuggestions(query: string): Promise<DataSourceSuggestion[] | null> {
34+
const span = getActiveSpan();
35+
36+
try {
37+
span?.setAttribute("query", query);
38+
span?.addEvent("query-get-data-source-suggestions");
39+
40+
const response = await (this.client as any).queryGetDataSourceSuggestions(query);
41+
42+
span?.setStatus({ code: SpanStatusCode.OK, message: "Data source suggestions retrieved" });
43+
44+
// Check if we have any data sources
45+
if (!response.dataSources || response.dataSources.length === 0) {
46+
span?.setAttribute("suggestions_count", 0);
47+
return null;
48+
}
49+
50+
span?.setAttribute("suggestions_count", response.dataSources.length);
51+
52+
// Return top 2 data sources (or just 1 if only 1 available)
53+
const topDataSources = response.dataSources.slice(0, 2);
54+
return topDataSources;
55+
56+
} catch (error) {
57+
span?.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message });
58+
console.error("Error getting data source suggestions: ", error);
59+
throw error;
60+
}
61+
}
62+
1363
/**
1464
* Get relevant questions for a given query and data sources
1565
*/
@@ -342,6 +392,7 @@ export class ThoughtSpotService {
342392
releaseVersion: info.releaseVersion,
343393
currentOrgId: info.currentOrgId,
344394
privileges: info.privileges,
395+
enableSpotterDataSourceDiscovery: info.enableSpotterDataSourceDiscovery,
345396
};
346397
}
347398

@@ -439,10 +490,18 @@ export async function getDataSources(
439490
return service.getDataSources();
440491
}
441492

493+
export async function getDataSourceSuggestions(
494+
query: string,
495+
client: ThoughtSpotRestApi,
496+
): Promise<DataSourceSuggestion[] | null> {
497+
const service = new ThoughtSpotService(client);
498+
return service.getDataSourceSuggestions(query);
499+
}
500+
442501
export async function getSessionInfo(client: ThoughtSpotRestApi): Promise<SessionInfo> {
443502
const service = new ThoughtSpotService(client);
444503
return service.getSessionInfo();
445504
}
446505

447506
// Export types
448-
export type { DataSource, SessionInfo } from "./types";
507+
export type { DataSource, SessionInfo, DataSourceSuggestion, DataSourceSuggestionResponse } from "./types";

src/thoughtspot/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ export interface DataSource {
44
description: string;
55
}
66

7+
export interface DataSourceSuggestion {
8+
confidence: number;
9+
header: {
10+
description: string;
11+
displayName: string;
12+
guid: string;
13+
};
14+
llmReasoning: string;
15+
}
16+
17+
export interface DataSourceSuggestionResponse {
18+
dataSources: DataSourceSuggestion[];
19+
}
20+
721
export interface SessionInfo {
822
mixpanelToken: string;
923
userGUID: string;
@@ -13,4 +27,5 @@ export interface SessionInfo {
1327
releaseVersion: string;
1428
currentOrgId: string;
1529
privileges: any;
30+
enableSpotterDataSourceDiscovery?: boolean;
1631
}

src/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,39 @@ export class McpServerError extends Error {
9090
}
9191
}
9292

93+
/**
94+
* Store a value in Cloudflare KV
95+
*/
96+
export async function putInKV(key: string, value: any, env?: any): Promise<void> {
97+
if (!env?.OAUTH_KV) {
98+
return;
99+
}
100+
try {
101+
await env.OAUTH_KV.put(key, JSON.stringify(value), { expirationTtl: 60 * 60 * 3 });
102+
} catch (error) {
103+
console.error('Error storing in KV:', error);
104+
}
105+
}
106+
107+
/**
108+
* Retrieve a value from Cloudflare KV
109+
*/
110+
export async function getFromKV(key: string, env?: any): Promise<any> {
111+
console.log('[DEBUG] Getting from KV', key);
112+
113+
if (!env?.OAUTH_KV) {
114+
return undefined;
115+
}
116+
117+
try {
118+
const value = await env.OAUTH_KV.get(key, { type: "json" });
119+
return value;
120+
} catch (error) {
121+
console.error('Error retrieving from KV:', error);
122+
return undefined;
123+
}
124+
}
125+
93126
export function instrumentedMCPServer<T extends BaseMCPServer>(MCPServer: new (ctx: Context) => T, config: ResolveConfigFn) {
94127
const Agent = class extends McpAgent<Env, any, Props> {
95128
server = new MCPServer(this);

0 commit comments

Comments
 (0)