Skip to content
Merged
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
67 changes: 0 additions & 67 deletions TASK.md
Original file line number Diff line number Diff line change
@@ -1,67 +0,0 @@
## Redis ๊ธฐ๋ฐ˜ ์ž„๋ฒ ๋”ฉ ํ ๋„์ž… ๊ณ„ํš

### 1. ์ ํ•ฉ์„ฑ ๊ฒ€ํ† 
- ๊ธฐ์กด Node.js ์ž„๋ฒ ๋”ฉ API๋Š” ์œ ์ง€ํ•˜๋ฉด์„œ๋„ Redis ํ๋ฅผ ์‚ฌ์ด์— ๋‘๋ฉด Spring Boot โ†’ Node.js ๊ฐ„์˜ ๋А์Šจํ•œ ๊ฒฐํ•ฉ๊ณผ ์žฌ์‹œ๋„๋ฅผ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์Œ.
- Spring Boot ํ”„๋กœ๋“€์„œ๋Š” ์ด๋ฏธ Redis LPUSH ๋กœ์ง์„ ๋ณด์œ ํ•˜๊ณ  ์žˆ์–ด ์ถ”๊ฐ€ ๊ฐœ๋ฐœ ๋ถ€๋‹ด์ด ๋‚ฎ์Œ.
- Node.js ์ปจ์Šˆ๋จธ๋Š” BRPOP ๊ธฐ๋ฐ˜ ๋ฌดํ•œ ๋ฃจํ”„๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ํ˜„์žฌ OpenAI ์ž„๋ฒ ๋”ฉ ํ˜ธ์ถœ ํ๋ฆ„๊ณผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์—ฐ๊ฒฐ๋จ.
- Redis ๋ฆฌ์ŠคํŠธ๋Š” ์„ ์ž…์„ ์ถœ ํŠน์„ฑ์„ ์ œ๊ณตํ•˜๊ณ , ์žฅ์•  ์‹œ ์‹คํŒจ ํ(`embedding:failed`)๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์šด์˜ํŒ€์ด ๋ชจ๋‹ˆํ„ฐ๋ง/์žฌ์ฒ˜๋ฆฌํ•˜๊ธฐ ์šฉ์ดํ•จ.
- ๊ณ ๊ฐ€์šฉ์„ฑ Redis ์ธํ”„๋ผ๊ฐ€ ์ „์ œ๋ผ์•ผ ํ•˜๋ฉฐ, ํ ์ ์ฒด/์ค‘๋ณต ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ์•Œ๋žŒ ์ฒด๊ณ„๊ฐ€ ํ•„์š”ํ•จ.

### 2. ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”
```
[Spring Boot] โ†’ LPUSH โ†’ [Redis List embedding:queue] โ†’ BRPOP โ†’ [Node.js Consumer] โ†’ OpenAI ์ž„๋ฒ ๋”ฉ โ†’ DB ์ €์žฅ
โ†˜ ์‹คํŒจ ์‹œ LPUSH embedding:failed
```

### 3. ๊ตฌํ˜„ ๊ณ„ํš
1. **์ปจ์Šˆ๋จธ ์›Œ์ปค ์ดˆ์•ˆ ์ž‘์„ฑ**
- `services/embedding.service.ts` ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” `processEmbeddingQueue` ๋ชจ๋“ˆ ์ž‘์„ฑ.
- Graceful shutdown, Concurrency(๋™์‹œ ์›Œ์ปค ์ˆ˜) ์˜ต์…˜, ๋กœ๊น…(์„ฑ๊ณต/์‹คํŒจ/์ฒ˜๋ฆฌ์‹œ๊ฐ„)์„ ํฌํ•จ.
2. **ํ ๋ฉ”์‹œ์ง€ ์Šคํ‚ค๋งˆ ํ™•์ •**
- `post_id`, `title`, `content`, `retryCount` ๋“ฑ์„ ํฌํ•จํ•˜๋Š” JSON ๊ตฌ์กฐ ์ •์˜ ๋ฐ ๋ฌธ์„œํ™”.
- ์ถ”ํ›„ schema ๋ณ€๊ฒฝ ๋Œ€๋น„ ๋ฒ„์ „ ํ•„๋“œ ๋„์ž… ๊ฒ€ํ† .
3. **์‹คํŒจ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ์ •์ฑ…**
- ์‹คํŒจ ์‹œ `embedding:failed` ๋กœ ์ด๋™ ํ›„ ๊ฒฝ๊ณ  ๋กœ๊ทธ ๊ธฐ๋ก.
- ์žฌ์‹œ๋„ ์›Œ์ปค(์ฃผ๊ธฐ์  RPOP โ†’ LPUSH) ๋˜๋Š” Ops ์ˆ˜๋™ ํŠธ๋ฆฌ๊ฑฐ ์ „๋žต ๊ฒฐ์ •.
4. **์šด์˜ ๋ชจ๋‹ˆํ„ฐ๋ง**
- `LLEN embedding:queue`, `embedding:failed` ๋ฉ”ํŠธ๋ฆญ์„ Prometheus/Grafana ๋˜๋Š” ๊ธฐ์กด ๋ชจ๋‹ˆํ„ฐ๋ง์— ์—ฐ๋™.
- ์•Œ๋žŒ ๊ธฐ์ค€: ํ ๊ธธ์ด ์ž„๊ณ„์น˜, ์‹คํŒจ ํ ๋ˆ„์ , ์›Œ์ปค ๋ฏธ์‘๋‹ต.
5. **๋ฐฐํฌ ์ „๋žต**
- Node.js ์ปจ์Šˆ๋จธ๋ฅผ ๊ธฐ์กด ์„œ๋ฒ„ ํ”„๋กœ์„ธ์Šค์™€ ๋ถ„๋ฆฌ( Docker ์ปจํ…Œ์ด๋„ˆ)ํ•˜์—ฌ ๋…๋ฆฝ ์šด์˜.
- Spring Boot ์ธก์€ ์ด๋ฏธ ๊ตฌํ˜„๋œ LPUSH ๋กœ์ง์„ ํ™œ์„ฑํ™”ํ•˜๊ณ , ๊ธฐ์กด REST ์ž„๋ฒ ๋”ฉ ํ˜ธ์ถœ์€ ์ ์ง„์ ์œผ๋กœ ๊ฐ์ถ•.

### 4. ์ถ”๊ฐ€ ๊ณ ๋ ค์‚ฌํ•ญ
- ๋ฉฑ๋“ฑ์„ฑ ํ™•๋ณด๋ฅผ ์œ„ํ•ด ์ปจ์Šˆ๋จธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ›„ Redis ์ธก์—์„œ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ฑฐํ–ˆ๋Š”์ง€(์ด๋ฏธ BRPOP ๋กœ ์ œ๊ฑฐ) ํ™•์ธํ•˜๊ณ , ์‹คํŒจ ์žฌ์ฒ˜๋ฆฌ ์‹œ ์ค‘๋ณต ์‚ฝ์ž… ๋ฐฉ์ง€ ๋กœ์ง ๊ฒ€ํ† .
- OpenAI API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ exponential backoff ์ ์šฉ ์—ฌ๋ถ€.
- ๊ธด ์ฝ˜ํ…์ธ  ์ž„๋ฒ ๋”ฉ ์‹œ chunking ๋กœ์ง(`chunkText`)๊ณผ ํ ๋ฉ”์‹œ์ง€ ํฌ๊ธฐ ์ œํ•œ ๊ฒ€ํ† .
- ๋ณด์•ˆ: Redis ์ ‘๊ทผ ์ œ์–ด, TLS ํ•„์š” ์—ฌ๋ถ€ ํ™•์ธ.

### 5. ๋‹ค์Œ ๋‹จ๊ณ„
- [x] Node.js ์ปจ์Šˆ๋จธ ์ดˆ์•ˆ ์ฝ”๋“œ ์ž‘์„ฑ ๋ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜(`REDIS_URL`, `EMBEDDING_QUEUE_KEY`) ์ •๋ฆฌ.
- [ ] ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ Redis ๋กœ์ปฌ ์ธ์Šคํ„ด์Šค์™€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ง„ํ–‰.
- [ ] ๋ชจ๋‹ˆํ„ฐ๋ง/์•Œ๋žŒ ๊ตฌ์„ฑ ๋…ผ์˜.

### 6. ์ปจํ…Œ์ด๋„ˆ/๋ฐฐํฌ ์„ค๊ณ„
- **๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ์žฌ์‚ฌ์šฉ**: ๊ธฐ์กด `Dockerfile` ๋กœ ๋นŒ๋“œํ•œ ๋™์ผ ์ด๋ฏธ์ง€๋ฅผ `api`(Express)์™€ `worker`(์ปจ์Šˆ๋จธ)๊ฐ€ ๊ณต์œ , ๊ฐ ์ปจํ…Œ์ด๋„ˆ๋Š” `command` ๋งŒ ๋‹ค๋ฅด๊ฒŒ ์ง€์ •.
- **์—”ํŠธ๋ฆฌํฌ์ธํŠธ ๋ถ„๋ฆฌ**: `src/worker/queue-consumer.ts` ์ถ”๊ฐ€ โ†’ `tsc` ๊ฒฐ๊ณผ๊ฐ€ `dist/worker/queue-consumer.js` ๋กœ ์ƒ์„ฑ๋˜๋„๋ก ๋นŒ๋“œ ๊ฒฝ๋กœ ํ™•์ธ. `package.json` ์— `worker` ์Šคํฌ๋ฆฝํŠธ(`node dist/worker/queue-consumer.js`) ๋“ฑ๋ก.
- **Docker Compose ์ดˆ์•ˆ**
```yaml
services:
api:
build: .
command: ["node", "dist/server.js"]
ports: ["3000:3000"]
env_file: .env
depends_on: [redis]

worker:
build: .
command: ["node", "dist/worker/queue-consumer.js"]
env_file: .env
depends_on: [redis]

redis:
image: redis:7-alpine
```
- **ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ณต์œ **: `.env` ์— Redis ์ ‘์† ์ •๋ณด(`REDIS_HOST`, `REDIS_PORT`, `REDIS_URL` ๋“ฑ)์™€ ํ ์ด๋ฆ„, ์‹คํŒจ ํ ์ด๋ฆ„ ๋“ฑ์„ ๋ช…์‹œํ•˜๊ณ  ๋‘ ์„œ๋น„์Šค ๋ชจ๋‘ ๋กœ๋“œ.
- **์šด์˜ ๊ณ ๋ ค**: `worker` ์ปจํ…Œ์ด๋„ˆ ์Šค์ผ€์ผ ์•„์›ƒ(์˜ˆ: `docker compose up --scale worker=3`)์— ๋Œ€๋น„ํ•ด ์ž‘์—… ๋ฉฑ๋“ฑ์„ฑ ํ™•์ธ. ์žฅ์•  ์‹œ ๊ฐœ๋ณ„ ์ปจํ…Œ์ด๋„ˆ ์žฌ์‹œ์ž‘ ์ „๋žต, ๋กœ๊ทธ ์ˆ˜์ง‘ ๊ฒฝ๋กœ(์˜ˆ: stdoutโ†’EFK) ์ •์˜.
4 changes: 3 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import aiRouter from './routes/ai.routes';
import aiV2Router from './routes/ai.v2.routes';
import searchRouter from './routes/search.routes';

// Express ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ๊ณต์šฉ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ดˆ๊ธฐํ™”
const app: Express = express();

// CORS ์„ค์ •
Expand All @@ -26,7 +27,8 @@ app.use('/ai', aiRouter);
app.use('/ai/v2', aiV2Router);
app.use('/search', searchRouter);

// Central Error Handler
// ์ค‘์•™ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ธฐ
// ์ „์—ญ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ธฐ๋กœ ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ์„œ๋ฒ„ ์˜ค๋ฅ˜๋ฅผ JSON์œผ๋กœ ๋ฐ˜ํ™˜
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod';

dotenv.config();

// ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์Šคํ‚ค๋งˆ๋กœ ๊ฒ€์ฆํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „ํ•œ ์„ค์ • ๊ฐ์ฒด ์ƒ์„ฑ
const configSchema = z.object({
NODE_ENV: z.string().default('development'),
PORT: z.coerce.number().default(3000),
Expand Down
19 changes: 11 additions & 8 deletions src/controllers/ai.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const embedTitleHandler = async (
res: Response,
next: NextFunction
) => {
// ํฌ์ŠคํŠธ ์ œ๋ชฉ ์ž„๋ฒ ๋”ฉ์„ ์ƒ์„ฑํ•˜๊ณ  ์ €์žฅ
try {
const { post_id, title } = req.body;
await storeTitleEmbedding(post_id, title);
Expand All @@ -28,6 +29,7 @@ export const embedContentHandler = async (
res: Response,
next: NextFunction
) => {
// ๋ณธ๋ฌธ์„ ์ฒญํฌ ๋‹จ์œ„๋กœ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ํ›„ DB์— ๋ฐ˜์˜
try {
const { post_id, content } = req.body;
const chunks = chunkText(content);
Expand All @@ -49,29 +51,30 @@ export const askHandler = async (
res: Response,
next: NextFunction
) => {
// RAG ๊ธฐ๋ฐ˜ QA ๊ฒฐ๊ณผ๋ฅผ SSE ์ŠคํŠธ๋ฆผ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌ
try {
const { question, user_id, category_id, speech_tone, post_id, llm } = req.body as any;

// SSE headers and anti-buffering hints
// SSE๋ฅผ ์œ„ํ•œ ํ—ค๋” ์„ค์ •๊ณผ ๋ฒ„ํผ๋ง ์™„ํ™” ์˜ต์…˜
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
// Nginx buffering off
// Nginx ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™”
res.setHeader('X-Accel-Buffering', 'no');
// Flush headers early so clients start processing immediately
// ํ—ค๋”๋ฅผ ๋จผ์ € ์ „์†กํ•ด ํด๋ผ์ด์–ธํŠธ ์ฒ˜๋ฆฌ๋ฅผ ์ฆ‰์‹œ ์‹œ์ž‘
(res as any).flushHeaders?.();
// Reduce Nagleโ€™s algorithm buffering on the socket for faster flush
// ์†Œ์ผ“์˜ ๋„ค์ด๊ธ€ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ฒ„ํผ๋ง์„ ์ค„์—ฌ ์ „์†ก ์ง€์—ฐ ์™„ํ™”
(res.socket as any)?.setNoDelay?.(true);
// Prime the SSE stream to break proxy buffering thresholds
// ํ”„๋ก์‹œ ๋ฒ„ํผ๋ง ์ž„๊ณ„๊ฐ’์„ ๋„˜๊ธฐ๊ธฐ ์œ„ํ•œ ์ดˆ๊ธฐ keep-alive ์ „์†ก
res.write(':ok\n\n');

const stream = await answerStream(question, user_id, category_id, speech_tone, post_id, llm);
// Manually bridge to ensure flushing of SSE deltas
// SSE ๋ธํƒ€๊ฐ€ ์ฆ‰์‹œ ์ „์†ก๋˜๋„๋ก ์ˆ˜๋™ ๋ธŒ๋ฆฌ์ง•
stream.on('data', (chunk) => {
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
res.write(buf);
const canFlush = typeof (res as any).flush === 'function';
// try to flush if supported by runtime/middleware
// ๋Ÿฐํƒ€์ž„ ๋˜๋Š” ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์ง€์›ํ•˜๋ฉด ์ฆ‰์‹œ ํ”Œ๋Ÿฌ์‹œ
(res as any).flush?.();
DebugLogger.log('sse', { type: 'debug.sse.write', at: Date.now(), bytes: buf.length, flushed: canFlush });
});
Expand All @@ -82,7 +85,7 @@ export const askHandler = async (
res.end();
});

// Cleanup if client disconnects
// ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ์ด ๋Š๊ธฐ๋ฉด ์ŠคํŠธ๋ฆผ ์ž์› ํ•ด์ œ
req.on('close', () => {
try {
stream.destroy();
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/ai.v2.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const askV2Handler = async (
res: Response,
next: NextFunction
) => {
// ๊ฒ€์ƒ‰ ๊ณ„ํš ๊ธฐ๋ฐ˜ v2 QA๋ฅผ SSE๋กœ ์ค‘๊ณ„
try {
const { question, user_id, category_id, speech_tone, post_id, llm } = req.body as any;
res.setHeader('Content-Type', 'text/event-stream');
Expand All @@ -26,4 +27,3 @@ export const askV2Handler = async (
next(error);
}
};

1 change: 1 addition & 0 deletions src/controllers/search.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { runHybridSearch } from '../services/hybrid-search.service';
import { runSemanticSearch } from '../services/semantic-search.service';
import { aggregatePosts } from '../services/search-aggregate.service';

// ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ API๋กœ ์งˆ๋ฌธ์„ ๋ฐ›์•„ ๊ฒ€์ƒ‰ ๊ณ„ํš๊ณผ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜
export const hybridSearchHandler = async (req: Request, res: Response) => {
try {
const question = String(req.query.question || '').trim();
Expand Down
13 changes: 7 additions & 6 deletions src/llm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import config from '../config';
import { randomUUID } from 'crypto';
import { DebugLogger } from '../utils/debug-logger';

// LLM ์ œ๊ณต์ž๋ณ„ ์ŠคํŠธ๋ฆผ์„ ์ถ”์ƒํ™”ํ•˜์—ฌ ๊ณตํ†ต SSE ํฌ๋งท์œผ๋กœ ๋ฐ˜ํ™˜
export const generate = async (req: GenerateRequest): Promise<PassThrough> => {
const merged = { ...req };
if (!merged.provider || !merged.model) {
Expand All @@ -23,13 +24,13 @@ export const generate = async (req: GenerateRequest): Promise<PassThrough> => {
const model = merged.model as string;
const provider = merged.provider as string;

// Pre-call logging: prompt tokens + estimated input cost
// ํ˜ธ์ถœ ์ „์— ํ”„๋กฌํ”„ํŠธ ํ† ํฐ ์ˆ˜์™€ ์˜ˆ์ƒ ์ž…๋ ฅ ๋น„์šฉ์„ ๊ธฐ๋ก
const messages = merged.messages || [];
let promptTokens = 0;
try {
promptTokens = countChatMessagesTokens(messages as any, model);
} catch {
// ignore
// ๋ฌด์‹œ
}
const pricing = getModelPricing(model);
const estInputCost = pricing ? calcCost(promptTokens, pricing.input_per_1k) : 0;
Expand Down Expand Up @@ -63,20 +64,20 @@ export const generate = async (req: GenerateRequest): Promise<PassThrough> => {
return s;
})();

// Wrap provider stream to accumulate output tokens
// ๊ณต๊ธ‰์ž ์ŠคํŠธ๋ฆผ์„ ๊ฐ์‹ธ ์ถœ๋ ฅ ํ† ํฐ์„ ์ง‘๊ณ„
const outer = new PassThrough();
let buffer = '';
let outputText = '';

// Debug: start info
// ๋””๋ฒ„๊ทธ: ํ˜ธ์ถœ ์‹œ์ž‘ ์ •๋ณด
try {
DebugLogger.log('llm', { type: 'debug.llm.start', provider, model, messages: (messages || []).length });
} catch {}

const flushBuffer = () => {
// Split by double newline to get SSE events
// ๋‘ ์ค„๋ฐ”๊ฟˆ์„ ๊ธฐ์ค€์œผ๋กœ SSE ์ด๋ฒคํŠธ ๋‹จ์œ„๋กœ ๋ถ„ํ• 
const chunks = buffer.split('\n\n');
// Keep last partial
// ๋งˆ์ง€๋ง‰ ๋ฏธ์™„์„ฑ ์กฐ๊ฐ์€ ๋ฒ„ํผ์— ๋ณด์กด
buffer = chunks.pop() || '';
for (const block of chunks) {
const lines = block.split('\n');
Expand Down
4 changes: 2 additions & 2 deletions src/llm/model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ type ModelEntry = {
modelId: string;
};

// Minimal registry for now; can expand with tokenizer/pricing later.
// ํ˜„์žฌ๋Š” ๊ธฐ๋ณธ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋งŒ ์ œ๊ณตํ•˜๋ฉฐ ์ถ”ํ›„ ํ† ํฌ๋‚˜์ด์ €๋‚˜ ๊ณผ๊ธˆ ์ •๋ณด๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ
const DEFAULT_CHAT: ModelEntry = { provider: 'openai', modelId: 'gpt-5-mini' };

// ํ”„๋กœ์ ํŠธ ๊ธฐ๋ณธ ์ฑ„ํŒ… ๋ชจ๋ธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜
export const getDefaultChat = (): ModelEntry => DEFAULT_CHAT;

10 changes: 5 additions & 5 deletions src/llm/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import { PassThrough } from 'stream';
import config from '../../config';
import { GenerateRequest } from '../types';

// Using @google/genai per project plan; keep types loose for compatibility
// ํ”„๋กœ์ ํŠธ ๊ณ„ํš์— ๋”ฐ๋ผ @google/genai๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ํƒ€์ž…์„ ๋А์Šจํ•˜๊ฒŒ ์œ ์ง€
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { GoogleGenAI } = require('@google/genai');

const buildPromptFromMessages = (messages: { role: string; content: string }[]) => {
// Simple concatenation preserving roles
// ์—ญํ•  ์ •๋ณด๋ฅผ ์œ ์ง€ํ•œ ์ฑ„ ๋‹จ์ˆœ ์—ฐ๊ฒฐ
return messages
.map((m) => `[${m.role}]\n${m.content}`)
.join('\n\n');
};

// Gemini SDK๋ฅผ ํ˜ธ์ถœํ•ด ์‘๋‹ต ํ…์ŠคํŠธ๋ฅผ SSE๋กœ ๋ถ„ํ• 
export const generateGeminiStream = async (req: GenerateRequest): Promise<PassThrough> => {
const stream = new PassThrough();
try {
Expand All @@ -37,7 +38,7 @@ export const generateGeminiStream = async (req: GenerateRequest): Promise<PassTh
const thinkingBudget = parseInt(process.env.GEMINI_THINKING_BUDGET || '0', 10) || 0;
const configBlock: any = thinkingBudget > 0 ? { thinkingConfig: { thinkingBudget } } : {};

// Non-streaming first, then chunk SSE
// ๋จผ์ € ๋™๊ธฐ ํ˜ธ์ถœ๋กœ ์‘๋‹ต์„ ๋ฐ›๊ณ  ์ดํ›„ SSE ์กฐ๊ฐ์œผ๋กœ ๋ถ„ํ• 
const result = await ai.models.generateContent({
model: modelId,
contents: [
Expand All @@ -50,7 +51,7 @@ export const generateGeminiStream = async (req: GenerateRequest): Promise<PassTh
config: configBlock,
});

// Try common text access paths
// ์‘๋‹ต ํ…์ŠคํŠธ๋ฅผ ์–ป๊ธฐ ์œ„ํ•œ ์—ฌ๋Ÿฌ ์ ‘๊ทผ ๊ฒฝ๋กœ๋ฅผ ์‹œ๋„
const outputText = (result?.response?.text && result.response.text()) || (result?.text && result.text()) || '';

const finalText = typeof outputText === 'string' ? outputText : '';
Expand All @@ -72,4 +73,3 @@ export const generateGeminiStream = async (req: GenerateRequest): Promise<PassTh
return stream;
}
};

Loading