Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions visual-embed/spotter/spotter-agent-embed/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# spotter-agent-embed
# Spotter Agent Embed

This is a small example of how embed spotter into your own agent if you have one. The example creates a simple agent using Gemini-flash model's function calling capability.

Expand All @@ -9,33 +9,69 @@ the actual API call to run the query on ThoughtSpot and return the ThoughtSpot v

Open in [Codesandbox](https://githubbox.com/thoughtspot/developer-examples/tree/main/visual-embed/spotter/spotter-agent-embed)

## Documentation
## 🚀 **Run Locally**

- [API Reference](https://developers.thoughtspot.com/docs/Class_BodylessConversation) for the Spotter Agent Embed.
- Full [tutorial](https://developers.thoughtspot.com/docs/tutorials/spotter/integrate-into-chatbot) on how to embed in your own chatbot.
```bash
# Get repo locally
git clone https://github.com/thoughtspot/developer-examples
cd visual-embed/spotter/spotter-agent-embed
# Install dependencies
npm install

## Environment
# Start both services (Gemini agent + React app)
npm start
```

The `.env` file contains some default values. Change the value of `VITE_THOUGHTSPOT_HOST` and `VITE_TS_DATASOURCE_ID` to use on your own instance.
## 📋 **Environment Variables**

## Run locally
```bash
# ThoughtSpot Configuration
VITE_THOUGHTSPOT_HOST=your-thoughtspot-host
VITE_TOKEN_SERVER=your-token-server
VITE_USERNAME=your-username
VITE_TS_DATASOURCE_ID=your-worksheet-id

# Gemini Configuration
GEMINI_API_KEY=your-gemini-api-key
AGENT_PORT=4000
```
$ git clone https://github.com/thoughtspot/developer-examples
$ cd visual-embed/spotter/spotter-agent-embed
```
```
$ npm i
```

## 🔄 **How It Works**

```
$ npm start
User: "Sales per year"
Gemini AI decides to call analyzeData function
ThoughtSpot Spotter generates visualization
SpotterMessage displays the chart
```

## Structure
## 📁 **Simple File Structure**

- **`src/App.tsx`** - Main chat interface (everything in one file!)
- **`api/simple-agent.ts`** - Gemini agent with function calling
- **`src/service.ts`** - Simple API calls to Gemini agent

## 💬 **Try These Queries**

**Text Responses:**
- "Hello"
- "What can you help me with?"

**Data Visualization:**
- "sales per year"
- "taxes this year"
- "Analyze revenue trends"

## 📚 **Learn More**

- `api/simple-agent.ts` A simple agent node service, using Gemini. This would be your own agent.
- `src/` React code for a chatbot using [Antd Pro chat](https://pro-chat.antdigital.dev/en-US/components/pro-chat#programming-operation-control)
- [ThoughtSpot Visual Embed SDK](https://developers.thoughtspot.com/docs/visual-embed-sdk)
- [Google Gemini Function Calling](https://ai.google.dev/docs/function_calling)
- [ProChat Documentation](https://pro-chat.antdigital.dev/)

---

### Technology labels

Expand Down
102 changes: 70 additions & 32 deletions visual-embed/spotter/spotter-agent-embed/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,94 @@
import React from 'react'
import React, { useEffect, useState } from 'react';
import { ProChat } from '@ant-design/pro-chat';
import { Flexbox } from 'react-layout-kit';
import { useTheme } from 'antd-style';
import { AuthType, init } from '@thoughtspot/visual-embed-sdk';
import { SpotterMessage, useSpotterAgent } from '@thoughtspot/visual-embed-sdk/react';
import { sendMessage, startAgentChat } from './service';

import { SpotterMessage } from './thoughtspot';
import './App.css';

import './App.css'
import { sendMessage, startAgentChat } from './service';

const tokenServer = import.meta.env.VITE_TOKEN_SERVER;
const username = import.meta.env.VITE_USERNAME;
const thoughtSpotHost = import.meta.env.VITE_THOUGHTSPOT_HOST;
const tsDatasourceId = import.meta.env.VITE_TS_DATASOURCE_ID;

init({
thoughtSpotHost,
authType: AuthType.TrustedAuthTokenCookieless,
getAuthToken: async () => {
const res = await fetch(`${tokenServer}/api/gettoken/${username}`);
return res.text();
}
});

function App() {
const theme = useTheme();
const [chatId, setChatId] = React.useState("");
const [chatId, setChatId] = useState('');
const [lastTSResponse, setLastTSResponse] = useState<any | null>(null);

const { sendMessage: sendSpotterMessage } = useSpotterAgent({
worksheetId: tsDatasourceId,
});

React.useEffect(() => {
startAgentChat().then(({chatId}) => {
setChatId(chatId);
});
useEffect(() => {
startAgentChat().then(({ chatId }) => setChatId(chatId));
}, []);

const handleMessage = async (messages: any[]) => {
const userMessage = messages[messages.length - 1].content;

try {
const geminiResponse = await sendMessage(chatId, userMessage);
const part = geminiResponse?.response?.candidates?.[0]?.content?.parts?.[0];

if (part?.functionCall?.name === 'analyzeData') {
const query = part.functionCall.args?.query;

const tsResponse = await sendSpotterMessage(query);
setLastTSResponse(tsResponse);
return new Response(`@spotter/${Date.now()}`);
}

if (part?.text) {
return new Response(part.text);
}

return new Response('No response from AI');

} catch (error) {
return new Response(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};

return (
<div style={{ width: '100%', height: '100%', background: theme.colorBgLayout }}>
<ProChat
loading={!chatId}
style={{ width: '100%', height: '100%' }}
style={{ width: '100%', height: '100%' }}
locale="en-US"
request={handleMessage}
chatItemRenderConfig={{
contentRender(props, defaultDom) {
try {
const message = JSON.parse(props.message);
const part = message.response.candidates[0].content.parts[0];
if (part.functionCall) {
const { functionCall } = part;
if (functionCall.name === 'analyzeData') {
return <SpotterMessage query={functionCall.args.query} />;
}
} else {
return part.text;
}
} catch {
// ignore
const message = props.message;

if (typeof message === 'string' && message.startsWith('@spotter/') && lastTSResponse) {
return (
<SpotterMessage
message={lastTSResponse.message}
style={{ height: "600px" }}
/>
);
}
// const msg = JSON.parse(props.message);

if (typeof message === 'string') return message;

return defaultDom;
},
}}
request={async (messages) => {
return sendMessage(chatId, messages[messages.length - 1].content);
}
}}
></ProChat>
/>
</div>
)
);
}

export default App
export default App;
11 changes: 5 additions & 6 deletions visual-embed/spotter/spotter-agent-embed/src/service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
export const sendMessage = async (chatId: string, message: any) => {
return fetch('/api/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ chatId, message }),
const res = await fetch('/api/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chatId, message }),
});
return res.json();
};

export const startAgentChat = async () => {
Expand Down
40 changes: 0 additions & 40 deletions visual-embed/spotter/spotter-agent-embed/src/thoughtspot.tsx

This file was deleted.

Loading