Skip to content

Commit c4a1e05

Browse files
Move away from the sessionResponseMap
1 parent 0c1c71e commit c4a1e05

File tree

4 files changed

+86
-19
lines changed

4 files changed

+86
-19
lines changed

.censitive

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Format <globPattern>:[keyRegex]
2+
**/.env:.*
3+

workshop-ui/src/app/api/chat/route.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import OpenAI, { AzureOpenAI } from 'openai';
32
import fs from 'fs';
43
import path from 'path';
54
import { getAppSessionFromRequest, getChatSession, validateAppSession } from '@/lib/session';
@@ -9,17 +8,14 @@ import { trace, Span } from '@opentelemetry/api';
98
import { getExerciseByNameWithResponse } from '@/lib/exercise-file-manager';
109
import { executePython } from './codeExecutionTool';
1110
import { runOpenAI } from './openaiRunner';
11+
import { decrypt, encrypt } from '@/lib/encryption';
1212

1313
const tracer = trace.getTracer('ai-workshop-chat');
1414

15-
// In-memory store for session ID -> OpenAI response ID mapping
16-
// TODO: Persist this to a database later
17-
const sessionResponseMap = new Map<string, { previousResponseId: string; sessionInstanceId: string }>();
18-
1915
/**
2016
* @route POST /api/chat
2117
* @desc Send a chat message
22-
* @body { message: string, resetConversation: boolean }
18+
* @body { message: string, encryptedPreviousResponseId: string | undefined }
2319
* @urlParam exercise: string
2420
* @response 200 { response: string } or 400 { error: string }
2521
* @access Protected (any authenticated user/workshop)
@@ -34,7 +30,7 @@ export async function POST(request: NextRequest) {
3430
}
3531

3632
// Get body payload and query string parameters
37-
const { message, resetConversation } = await request.json();
33+
const { message, encryptedPreviousResponseId } = await request.json();
3834
if (!message || typeof message !== 'string') {
3935
return NextResponse.json({ error: 'Message is required' }, { status: 400 });
4036
}
@@ -60,14 +56,15 @@ export async function POST(request: NextRequest) {
6056
await session.save();
6157
}
6258

63-
// Handle conversation reset if requested
64-
if (resetConversation === true) {
65-
sessionResponseMap.delete(sessionId);
59+
let previousResponseId: string | undefined = undefined;
60+
if (encryptedPreviousResponseId) {
61+
// console.log('Received encryptedPreviousResponseId:', encryptedPreviousResponseId);
62+
try {
63+
previousResponseId = decrypt(encryptedPreviousResponseId, Buffer.from(process.env.PREVIOUS_RESPONSE_ID_SECRET!, 'hex'));
64+
} catch {
65+
}
6666
}
6767

68-
// Get previous response ID from in-memory store
69-
let { previousResponseId, sessionInstanceId } = sessionResponseMap.get(sessionId) || { previousResponseId: undefined, sessionInstanceId: crypto.randomUUID() };
70-
7168
// Read system prompt
7269
const systemPromptPath = path.join(process.cwd(), 'prompts', exerciseData.folder, exerciseData.system_prompt_file);
7370
let systemPrompt = await fs.promises.readFile(systemPromptPath, { encoding: 'utf-8' });
@@ -105,24 +102,35 @@ Achtung! Die Dateien haben mehr Zeilen als hier gezeigt. Alle Dateien sind im Or
105102
const result = await executePython(
106103
script,
107104
exerciseData.data_files.map(f => path.join(process.cwd(), 'prompts', exerciseData.folder, f)),
108-
sessionInstanceId
105+
sessionId
109106
);
110107
if (result.resultFiles) {
111108
for (const resultFile of result.resultFiles) {
109+
// TODO: handle non-image files differently (see GH issue #29)
112110
const markdownImage = `![Generated Image](${resultFile.url})`;
113111
const data = JSON.stringify({ delta: `\n\n${markdownImage}\n\n` });
114112
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
115113
}
116114
}
115+
if (result.stdout || result.stderr) {
116+
// TODO: send the output as a collapsed section (see GH issue #17)
117+
}
117118
return JSON.stringify(result);
118119
},
119120
welcomeMessage
120121
);
121122

122-
sessionResponseMap.set(sessionId, {
123-
previousResponseId: newPreviousResponseId,
124-
sessionInstanceId,
125-
});
123+
// Update session with new response ID
124+
// Encrypt previousResponseId before sending to client
125+
let encryptedResponseId: string | undefined;
126+
// console.log('New previousResponseId:', newPreviousResponseId);
127+
// console.log('Encryption key:', process.env.PREVIOUS_RESPONSE_ID_SECRET);
128+
// Revert key.toString('hex') first
129+
130+
encryptedResponseId = encrypt(newPreviousResponseId, Buffer.from(process.env.PREVIOUS_RESPONSE_ID_SECRET!, 'hex'));
131+
132+
const data = JSON.stringify({ encryptedResponseId });
133+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
126134

127135
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
128136
} catch (error) {

workshop-ui/src/app/chat/[exercise]/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default function Home() {
3535
const inputRef = useRef<HTMLTextAreaElement>(null);
3636
const dropdownRef = useRef<HTMLDivElement>(null);
3737
const [isFirstCall, setIsFirstCall] = useState(true);
38+
const [responseId, setResponseId] = useState<string | undefined>(undefined);
3839

3940
// Cache for data file content - only fetch once per component session
4041
const dataFileContentCache = useRef<string | null>(null);
@@ -158,7 +159,7 @@ export default function Home() {
158159
},
159160
body: JSON.stringify({
160161
message: userMessage,
161-
resetConversation: isFirstCall,
162+
encryptedPreviousResponseId: responseId,
162163
}),
163164
});
164165

@@ -217,6 +218,10 @@ export default function Home() {
217218
assistantMessage += parsed.delta;
218219
setCurrentBotMessage(assistantMessage);
219220
}
221+
if (parsed.encryptedResponseId) {
222+
setResponseId(parsed.encryptedResponseId);
223+
console.log('Received encryptedResponseId:', parsed.encryptedResponseId);
224+
}
220225
} catch {
221226
// Ignore parsing errors for SSE data
222227
}

workshop-ui/src/lib/encryption.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import crypto from 'crypto';
2+
3+
// Function to encrypt data
4+
export function encrypt(text: string, key: Buffer): string {
5+
const algorithm = 'aes-256-gcm';
6+
const iv = crypto.randomBytes(16); // Generate a random initialization vector
7+
8+
const cipher = crypto.createCipheriv(algorithm, key, iv);
9+
cipher.setAAD(Buffer.from('nextjs-app', 'utf8'));
10+
11+
let encrypted = cipher.update(text, 'utf8', 'hex');
12+
encrypted += cipher.final('hex');
13+
14+
const authTag = cipher.getAuthTag();
15+
16+
// Combine IV, auth tag, and encrypted data
17+
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
18+
}
19+
20+
// Function to decrypt data
21+
export function decrypt(encryptedText: string, key: Buffer): string {
22+
const algorithm = 'aes-256-gcm';
23+
24+
// Split the IV, auth tag, and encrypted data
25+
const textParts = encryptedText.split(':');
26+
const iv = Buffer.from(textParts[0], 'hex');
27+
const authTag = Buffer.from(textParts[1], 'hex');
28+
const encryptedData = textParts[2];
29+
30+
const decipher = crypto.createDecipheriv(algorithm, key, iv);
31+
decipher.setAAD(Buffer.from('nextjs-app', 'utf8'));
32+
decipher.setAuthTag(authTag);
33+
34+
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
35+
decrypted += decipher.final('utf8');
36+
37+
return decrypted;
38+
}
39+
40+
// Example usage
41+
// const key = crypto.randomBytes(32); // Generate a random 256-bit key
42+
// const textToEncrypt = 'Hello, World!';
43+
44+
// const hexKey = key.toString('hex');
45+
// console.log('Hexadecimal Key:', hexKey);
46+
47+
// const encrypted = encrypt(textToEncrypt, key);
48+
// console.log('Encrypted:', encrypted);
49+
50+
// const decrypted = decrypt(encrypted, key);
51+
// console.log('Decrypted:', decrypted);

0 commit comments

Comments
 (0)