| 2549 | } |
| 2550 | |
| 2551 | const char* rcli_rag_query(RCLIHandle handle, const char* query) { |
| 2552 | if (!handle || !query) return ""; |
| 2553 | auto* engine = static_cast<RCLIEngine*>(handle); |
| 2554 | if (!engine->rag_ready || !engine->rag_retriever || !engine->rag_embedding) { |
| 2555 | return "{\"error\": \"RAG not initialized. Run rcli_rag_ingest() first.\"}"; |
| 2556 | } |
| 2557 | |
| 2558 | std::lock_guard<std::mutex> lock(engine->mutex); |
| 2559 | |
| 2560 | auto query_embedding = engine->rag_embedding->embed(std::string(query)); |
| 2561 | if (query_embedding.empty()) { |
| 2562 | return "{\"error\": \"Failed to embed query\"}"; |
| 2563 | } |
| 2564 | |
| 2565 | auto results = engine->rag_retriever->retrieve( |
| 2566 | std::string(query), query_embedding, 5); |
| 2567 | |
| 2568 | if (results.empty()) { |
| 2569 | engine->last_rag_result = "{\"results\": [], \"answer\": \"No relevant documents found.\"}"; |
| 2570 | return engine->last_rag_result.c_str(); |
| 2571 | } |
| 2572 | |
| 2573 | // Build context from retrieved chunks |
| 2574 | std::string context; |
| 2575 | for (size_t i = 0; i < results.size(); i++) { |
| 2576 | context += "[" + std::to_string(i + 1) + "] " + std::string(results[i].text) + "\n\n"; |
| 2577 | } |
| 2578 | |
| 2579 | // Generate answer using LLM with retrieved context. |
| 2580 | // The system prompt enforces speech-friendly output since responses are |
| 2581 | // streamed to TTS — no bullets, numbered lists, markdown, or citations. |
| 2582 | std::string rag_system = rastack::apply_personality( |
| 2583 | "You are RCLI, a voice assistant that answers questions using provided documents. " |
| 2584 | "Your response will be spoken aloud by a text-to-speech engine, so you MUST follow these rules:\n" |
| 2585 | "- Answer in plain, natural conversational sentences only.\n" |
| 2586 | "- NEVER use bullet points, numbered lists, markdown, or any formatting.\n" |
| 2587 | "- NEVER include citations like [1], [2], or source references.\n" |
| 2588 | "- NEVER use parentheses for asides or abbreviations.\n" |
| 2589 | "- Do NOT use <think> tags or internal reasoning.\n" |
| 2590 | "- Keep your answer concise — 2 to 4 sentences maximum.\n" |
| 2591 | "- If the answer is not in the context, say so briefly.", |
| 2592 | engine->personality_key); |
| 2593 | |
| 2594 | std::string rag_prompt = |
| 2595 | "Context:\n" + context + "\n" |
| 2596 | "Question: " + std::string(query); |
| 2597 | |
| 2598 | std::string answer; |
| 2599 | if (engine->pipeline.using_metalrt()) { |
| 2600 | auto& mrt = engine->pipeline.metalrt_llm(); |
| 2601 | std::string rag_full = mrt.profile().build_chat_prompt(rag_system, {}, rag_prompt); |
| 2602 | answer = mrt.generate_raw(rag_full, nullptr); |
| 2603 | engine->metalrt_kv_continuation_len = 0; |
| 2604 | } else if (engine->pipeline.llm().is_initialized()) { |
| 2605 | answer = engine->pipeline.llm().generate( |
| 2606 | engine->pipeline.llm().build_chat_prompt(rag_system, {}, rag_prompt), |
| 2607 | nullptr); |
| 2608 | } else { |
no test coverage detected