@@ -1,2 +1 @@
|
||||
GOOGLE_API_KEY=АlzaSyBFGЗDLQnaPХ8H413uztWT9Tzcg_yCTNTk
|
||||
AZURE_OPENAI_KEY=o20WLCQfubbGTo0SnkS70lefG0tHvdZlzcUGHOPDmww0igy94Up0JQQJ99CEAC77bzfXJ3w3AAAAACOGIUYb
|
||||
@@ -1,53 +1,160 @@
|
||||
const axios = require('axios');
|
||||
require('dotenv').config();
|
||||
const { OpenAI } = require('openai');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
// 1. FIX: Format base endpoint to match standard Azure OpenAI layout
|
||||
const azureEndpoint = "https://cpmindiayoda-resource.services.ai.azure.com";
|
||||
const deploymentName = "gpt-4o-mini";
|
||||
const apiVersion = "2024-08-01-preview"; // Mandatory query param for Azure endpoints
|
||||
const apiVersion = "2024-08-01-preview";
|
||||
|
||||
// Configure the SDK compatibility routing
|
||||
const client = new OpenAI({
|
||||
baseURL: `${azureEndpoint}/openai/deployments/${deploymentName}`,
|
||||
apiKey: process.env.AZURE_OPENAI_KEY,
|
||||
defaultHeaders: { 'api-key': process.env.AZURE_OPENAI_KEY },
|
||||
// Inject the required api-version parameter into every request automatically
|
||||
defaultQuery: { 'api-version': apiVersion }
|
||||
});
|
||||
|
||||
console.log('Azure OpenAI Client Initialized with Deployment:', deploymentName);
|
||||
|
||||
const fetchWrenData = async (prompt, tenantId) => {
|
||||
try {
|
||||
return {
|
||||
question: "Monthly revenue share across electronics product categories",
|
||||
sql: `
|
||||
SELECT product_category, SUM(revenue) AS total_revenue, order_month
|
||||
FROM sales_pipeline
|
||||
WHERE order_year = 2026
|
||||
GROUP BY product_category, order_month
|
||||
ORDER BY order_month ASC, total_revenue DESC;
|
||||
`,
|
||||
data: [
|
||||
{ product_category: "Smartphones", total_revenue: 145000, order_month: "January" },
|
||||
{ product_category: "Laptops", total_revenue: 210000, order_month: "January" },
|
||||
{ product_category: "Audio Wearables", total_revenue: 65000, order_month: "January" },
|
||||
{ product_category: "Smartphones", total_revenue: 160000, order_month: "February" },
|
||||
{ product_category: "Laptops", total_revenue: 195000, order_month: "February" },
|
||||
{ product_category: "Audio Wearables", total_revenue: 85000, order_month: "February" }
|
||||
]
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Wren AI Integration Error:', error.message);
|
||||
throw new Error('Failed to fetch data payload from Wren AI endpoint');
|
||||
}
|
||||
|
||||
|
||||
const WREN_URL = "http://172.236.172.26:3000/api/graphql";
|
||||
|
||||
const gql = async (operationName, query, variables) => {
|
||||
const res = await axios.post(WREN_URL, { operationName, query, variables }, {
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
timeout: 60000,
|
||||
});
|
||||
if (res.data?.errors) throw new Error(res.data.errors[0].message);
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
const pollUntilFinished = async (taskId, maxAttempts = 50) => {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const { askingTask } = await gql("AskingTask",
|
||||
`query AskingTask($taskId: String!) {
|
||||
askingTask(taskId: $taskId) {
|
||||
status
|
||||
candidates { sql }
|
||||
error { message }
|
||||
}
|
||||
}`,
|
||||
{ taskId }
|
||||
);
|
||||
|
||||
console.log(`Poll ${i + 1} => ${askingTask?.status}`);
|
||||
|
||||
if (askingTask?.error) throw new Error(askingTask.error.message);
|
||||
|
||||
if (askingTask?.status === "FINISHED") {
|
||||
if (askingTask?.candidates?.length > 0) {
|
||||
return { sql: askingTask.candidates[0].sql, type: "sql" };
|
||||
}
|
||||
return {
|
||||
sql: null,
|
||||
type: "clarification",
|
||||
message: "I couldn't generate SQL for this question. Please try rephrasing.",
|
||||
};
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
throw new Error("Wren polling timeout");
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchWrenData = async (prompt) => {
|
||||
try {
|
||||
// Step 1: Create task
|
||||
const { createAskingTask } = await gql("CreateAskingTask",
|
||||
`mutation CreateAskingTask($data: AskingTaskInput!) {
|
||||
createAskingTask(data: $data) { id }
|
||||
}`,
|
||||
{ data: { question: prompt } }
|
||||
);
|
||||
console.log("Task =>", createAskingTask.id);
|
||||
|
||||
// Step 2: Poll for SQL
|
||||
const pollResult = await pollUntilFinished(createAskingTask.id);
|
||||
|
||||
// Clarification needed
|
||||
if (pollResult.type === "clarification") {
|
||||
return {
|
||||
success: false,
|
||||
type: "clarification",
|
||||
message: pollResult.message,
|
||||
data: [],
|
||||
chart: null,
|
||||
};
|
||||
}
|
||||
|
||||
const wrenSql = pollResult.sql;
|
||||
console.log("SQL ready");
|
||||
|
||||
// Step 3: Create thread
|
||||
const { createThread } = await gql("CreateThread",
|
||||
`mutation CreateThread($data: CreateThreadInput!) {
|
||||
createThread(data: $data) { id }
|
||||
}`,
|
||||
{ data: { question: prompt, sql: wrenSql } }
|
||||
);
|
||||
console.log("Thread =>", createThread.id);
|
||||
|
||||
// Step 4: Create thread response
|
||||
const { createThreadResponse } = await gql("CreateThreadResponse",
|
||||
`mutation CreateThreadResponse($threadId: Int!, $data: CreateThreadResponseInput!) {
|
||||
createThreadResponse(threadId: $threadId, data: $data) { id }
|
||||
}`,
|
||||
{ threadId: createThread.id, data: { question: prompt, sql: wrenSql } }
|
||||
);
|
||||
console.log("Response ID =>", createThreadResponse.id);
|
||||
|
||||
// Step 5: Preview data
|
||||
const { previewData } = await gql("PreviewData",
|
||||
`mutation PreviewData($where: PreviewDataInput!) {
|
||||
previewData(where: $where)
|
||||
}`,
|
||||
{ where: { responseId: parseInt(createThreadResponse.id) } }
|
||||
);
|
||||
|
||||
const columns = previewData.columns.map(c => c.name);
|
||||
const rows = previewData.data.map(row =>
|
||||
Object.fromEntries(columns.map((col, i) => [col, row[i]]))
|
||||
);
|
||||
|
||||
|
||||
|
||||
console.log(`Done — ${rows.length} rows`);
|
||||
console.table(rows);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: "data",
|
||||
prompt,
|
||||
sql: wrenSql,
|
||||
totalRows: rows.length,
|
||||
columns,
|
||||
data: rows,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error("WREN ERROR =>", err.message);
|
||||
return {
|
||||
success: false,
|
||||
type: "error",
|
||||
data: [],
|
||||
chart: null,
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const generateVegaSchema = async (question, dataArray) => {
|
||||
try {
|
||||
const systemPrompt = `You are a data visualization expert. I will provide a user's question and a JSON array of data. Your task is to generate a strictly valid Vega-Lite JSON specification to visualize this data. The data array will be provided to the Vega spec internally. Map the JSON keys to the correct x, y, and color axes. Choose the best chart type (bar, line, arc) based on the question.`;
|
||||
|
||||
|
||||
|
||||
const userPrompt = `User Question: "${question}"\nData JSON: ${JSON.stringify(dataArray)}`;
|
||||
const completion = await client.chat.completions.create({
|
||||
model: '',
|
||||
@@ -67,9 +174,63 @@ const generateVegaSchema = async (question, dataArray) => {
|
||||
}
|
||||
};
|
||||
|
||||
const generateSQL = async (prompt, mdl) => {
|
||||
|
||||
try {
|
||||
const mdlObject = yaml.load(mdl);
|
||||
const systemPrompt = `
|
||||
You are an expert SQL query generator.
|
||||
|
||||
STRICT RULES:
|
||||
1. Generate ONLY SQL query.
|
||||
2. No explanation.
|
||||
3. No markdown.
|
||||
4. Use ONLY columns from MDL.
|
||||
5. Use table name correctly.
|
||||
6. Respect expression fields.
|
||||
7. Use proper aggregation and GROUP BY.
|
||||
`;
|
||||
const userPrompt = `USER QUESTION:${prompt}MDL:${JSON.stringify(mdlObject, null, 2)}`;
|
||||
const completion =
|
||||
await client.chat.completions.create({
|
||||
model: '',
|
||||
temperature: 0,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: userPrompt
|
||||
}
|
||||
]
|
||||
|
||||
});
|
||||
const sql =
|
||||
completion
|
||||
.choices[0]
|
||||
.message
|
||||
.content
|
||||
.trim();
|
||||
return { prompt, sql };
|
||||
|
||||
} catch (error) {
|
||||
|
||||
|
||||
console.error(
|
||||
'Generate SQL Error:',
|
||||
error.message
|
||||
);
|
||||
|
||||
throw error;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handleOrchestration = async (req, res) => {
|
||||
try {
|
||||
const { prompt } = req.body;
|
||||
const { prompt, mdl } = req.body;
|
||||
const tenant_id = req.user ? req.user.client_id : null;
|
||||
|
||||
if (!prompt) {
|
||||
@@ -78,8 +239,13 @@ const handleOrchestration = async (req, res) => {
|
||||
if (!tenant_id) {
|
||||
return res.status(400).json({ error: 'Tenant context (client_id) missing from auth token' });
|
||||
}
|
||||
const promptwithsql = await generateSQL(prompt, mdl)
|
||||
// console.log('Prompt with SQL:', promptwithsql.prompt, promptwithsql.sql, tenant_id);
|
||||
// return res.status(200).json(promptwithsql);
|
||||
|
||||
const wrenData = await fetchWrenData(prompt, tenant_id);
|
||||
const wrenData = await fetchWrenData(promptwithsql.prompt, promptwithsql.sql, tenant_id);
|
||||
console.log('Wren Data =>', wrenData);
|
||||
return res.status(200).json(wrenData);
|
||||
const vegaSchema = await generateVegaSchema(wrenData.question, wrenData.data);
|
||||
|
||||
if (!vegaSchema || !vegaSchema.$schema) {
|
||||
@@ -94,8 +260,130 @@ const handleOrchestration = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { handleOrchestration };
|
||||
const generateVegaJson = async (queryResult) => {
|
||||
try {
|
||||
const systemPrompt = `You are a data visualization expert. I will provide a user's question and a JSON array of data. Your task is to generate a strictly valid Vega-Lite JSON specification to visualize this data. The data array will be provided to the Vega spec internally. Map the JSON keys to the correct x, y, and color axes. Choose the best chart type (bar,pai,line, arc) based on the question.`;
|
||||
|
||||
const userPrompt = `
|
||||
DATA:
|
||||
${JSON.stringify(queryResult, null, 2)}
|
||||
|
||||
Generate Vega-Lite JSON.
|
||||
`;
|
||||
|
||||
const completion = await client.chat.completions.create({
|
||||
model: "",
|
||||
temperature: 0,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: userPrompt
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const vegaJson = completion.choices[0].message.content.trim();
|
||||
const cleanJson = vegaJson
|
||||
.replace(/```json/g, "")
|
||||
.replace(/```/g, "")
|
||||
.trim();
|
||||
|
||||
return JSON.parse(cleanJson);
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error("Vega Generation Error =>", err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
const ask = async (req, res) => {
|
||||
try {
|
||||
const { prompt } = req.body;
|
||||
|
||||
if (!prompt?.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Prompt required"
|
||||
});
|
||||
}
|
||||
|
||||
const result = await fetchWrenData(prompt);
|
||||
|
||||
const vegaSpec = await generateVegaJson({
|
||||
columns: result.columns,
|
||||
data: result.data,
|
||||
chart: result.prompt,
|
||||
sql: result.sql
|
||||
});
|
||||
// console.log("Ask Result =>",vegaSpec);
|
||||
return res.json({
|
||||
...result,
|
||||
vegaSpec
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
// const ask = async (req, res) => {
|
||||
// const { prompt } = req.body;
|
||||
// if (!prompt?.trim()) {
|
||||
// return res.status(400).json({ success: false, error: "Prompt required" });
|
||||
// }
|
||||
// const result = await fetchWrenData(prompt);
|
||||
// console.log('Ask Result =>', result);
|
||||
|
||||
// res.json(result);
|
||||
// }
|
||||
|
||||
const getSuggestedQuestions = async () => {
|
||||
try {
|
||||
const { threads } = await gql(
|
||||
"Threads",
|
||||
`
|
||||
query Threads {
|
||||
threads {
|
||||
id
|
||||
summary
|
||||
}
|
||||
}
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
||||
return (threads || [])
|
||||
.filter(t => t.summary)
|
||||
.slice(0, 5)
|
||||
.map(t => ({
|
||||
question: t.summary,
|
||||
category: "Recent"
|
||||
}));
|
||||
|
||||
} catch (fallbackErr) {
|
||||
console.error(
|
||||
"Fallback failed =>",
|
||||
fallbackErr.message
|
||||
);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const suggestions = async (req, res) => {
|
||||
const questions = await getSuggestedQuestions();
|
||||
res.json({ success: true, questions });
|
||||
|
||||
}
|
||||
|
||||
module.exports = { handleOrchestration,ask,suggestions };
|
||||
|
||||
|
||||
|
||||
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fixed Vega-Lite Revenue Share Chart</title>
|
||||
<!-- CDN Links for Libraries -->
|
||||
<script src="https://jsdelivr.net"></script>
|
||||
<script src="https://jsdelivr.net"></script>
|
||||
<script src="https://jsdelivr.net"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f4f6f9;
|
||||
margin: 0;
|
||||
}
|
||||
#chart-container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- This div holds your live UI chart -->
|
||||
<div id="chart-container"></div>
|
||||
|
||||
<script>
|
||||
// सुधरा हुआ Vega-Lite JSON (सभी सिंटैक्स एरर्स फिक्स कर दिए गए हैं)
|
||||
const correctedSpec = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"title": "Monthly Revenue Share Across Electronics Product Categories",
|
||||
"data": {
|
||||
"values": [
|
||||
{ "product_category": "Smartphones", "total_revenue": 145000, "order_month": "January" },
|
||||
{ "product_category": "Laptops", "total_revenue": 210000, "order_month": "January" },
|
||||
{ "product_category": "Audio Wearables", "total_revenue": 65000, "order_month": "January" },
|
||||
{ "product_category": "Smartphones", "total_revenue": 160000, "order_month": "February" },
|
||||
{ "product_category": "Laptops", "total_revenue": 195000, "order_month": "February" },
|
||||
{ "product_category": "Audio Wearables", "total_revenue": 85000, "order_month": "February" }
|
||||
]
|
||||
},
|
||||
"transform": [
|
||||
{
|
||||
"joinaggregate": [{ "op": "sum", "field": "total_revenue", "as": "monthly_total" }],
|
||||
"groupby": ["order_month"]
|
||||
},
|
||||
{
|
||||
"calculate": "datum.total_revenue / datum.monthly_total",
|
||||
"as": "revenue_share"
|
||||
}
|
||||
],
|
||||
"mark": "bar",
|
||||
"encoding": {
|
||||
"x": { "field": "product_category", "type": "nominal", "axis": { "labelAngle": 0 }, "title": "Category" },
|
||||
"y": { "field": "revenue_share", "type": "quantitative", "axis": { "format": "%" }, "title": "Revenue Share" },
|
||||
"color": { "field": "product_category", "type": "nominal" }
|
||||
},
|
||||
"facet": {
|
||||
"columns": 2,
|
||||
"field": "order_month",
|
||||
"type": "nominal",
|
||||
"title": "Timeline"
|
||||
}
|
||||
};
|
||||
|
||||
// Render the view
|
||||
vegaEmbed('#chart-container', correctedSpec, { actions: false })
|
||||
.then(result => console.log("UI Render Success!"))
|
||||
.catch(console.error);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+2319
-1
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -11,6 +11,7 @@
|
||||
"start": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api/wrenai": "file:.api/apis/wrenai",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@clickhouse/client": "^1.18.5",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
@@ -18,8 +19,10 @@
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"nodemon": "^3.1.14",
|
||||
"openai": "^6.39.0"
|
||||
"openai": "^6.39.0",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,7 @@ const handleOrchestration = require('../controller/handleOrchestration.js');
|
||||
|
||||
router.post('/loginUser', loginUser.loginUser);
|
||||
router.post('/handleOrchestration', authMiddleware, handleOrchestration.handleOrchestration);
|
||||
router.post('/ask', handleOrchestration.ask);
|
||||
router.get('/suggestions', handleOrchestration.suggestions);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user