Onthesis Ai V2 Gemini Rules — Free Gemini Rules Template
    Neura MarketNeura Market/Gemini
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsGamesBlogVideosGuidesCoursesCommunityGemsExtensionsTrending
    GeminiRulesOnthesis Ai V2 Gemini Rules
    Back to Rules

    Onthesis Ai V2 Gemini Rules

    onthesis10 July 19, 2026
    0 copies 0 downloads
    Rule Content
    """
    agent.py — Thesis Agent API Routes
    Contains:
      - POST /api/agent/chat  (legacy synchronous endpoint)
      - POST /api/agent/run   (NEW: SSE streaming endpoint for AgentPanel)
    """
    
    import os
    import json
    import time
    import logging
    from datetime import datetime
    from flask import Blueprint, request, jsonify, Response, stream_with_context  # type: ignore
    from flask_login import login_required, current_user  # type: ignore
    from app.agent.memory_system import QdrantVectorDB, SharedMemory, FirestoreDocumentDB, count_tokens  # type: ignore
    
    logger = logging.getLogger(__name__)
    
    # Setup blueprint
    agent_api_bp = Blueprint('agent_api', __name__)
    
    # ──────────────────────────────────────────────
    # MODEL MAPPING — Frontend model IDs → litellm model strings
    # ──────────────────────────────────────────────
    MODEL_MAP = {
        "llama-70b":   "groq/llama-3.3-70b-versatile",
        "deepseek-r1": "groq/deepseek-r1-distill-llama-70b",
        "gemma-9b":    "groq/gemma2-9b-it",
    }
    FALLBACK_MODEL = "gemini/gemini-2.5-flash"
    DEFAULT_MODEL_KEY = "llama-70b"
    
    # ──────────────────────────────────────────────
    # SYSTEM PROMPT — Based on onthesis-agent-prompts.md
    # ──────────────────────────────────────────────
    AGENT_SYSTEM_PROMPT = """Kamu adalah thesis agent yang membantu user menulis tesis.
    Gunakan tools yang tersedia untuk membaca, mengedit, dan mencari referensi.
    
    IMPORTANT RULES:
    1. Write in the user's language (default: Indonesian/Bahasa Indonesia).
    2. Use academic formal style: avoid casual words like "guys", "banget", "nih".
    3. Use passive voice for methodology: "dilakukan", "dianalisis", "diperoleh".
    4. Use hedging language for claims: "menunjukkan", "mengindikasikan", "cenderung".
    5. Paragraph structure: Topic sentence -> Evidence -> Analysis -> Transition.
    6. Never fabricate citations. Only reference papers from the provided context.
    7. After making changes, explain what you did and why.
    8. If the user's request is conversational (greeting, question), respond naturally with concise text.
    
    OUTPUT FORMAT:
    After making edits, write a brief summary of changes made.
    End with a suggestion for next steps:
    "Langkah berikutnya: ..."
    """
    
    
    def _build_context_message(context: dict) -> str:
        """Build a context injection string from the frontend context object."""
        parts = []
        
        title = context.get("context_title", "")
        if title:
            parts.append(f"Judul Tesis: {title}")
        
        problem = context.get("context_problem", "")
        if problem:
            parts.append(f"Rumusan Masalah: {problem}")
        
        method = context.get("context_method", "")
        if method:
            parts.append(f"Metodologi: {method}")
        
        # Active paragraphs summary
        paragraphs = context.get("active_paragraphs", [])
        if paragraphs:
            para_summary = []
            for p in paragraphs[:20]:  # Cap display
                pid = p.get("paraId", "?")
                content = p.get("content", "")
                preview = content[:100] + "..." if len(content) > 100 else content
                para_summary.append(f"  [{pid}] {preview}")
            parts.append(f"Paragraf Aktif ({len(paragraphs)} total):\n" + "\n".join(para_summary))
        
        # References
        refs = context.get("references_text", "")
        if refs:
            parts.append(f"Referensi Tersedia:\n{refs}")
        
        # Golden thread
        gt = context.get("golden_thread", {})
        if gt and gt.get("researchQuestion"):
            parts.append(f"Research Question: {gt['researchQuestion']}")
        
        # Chapter summaries
        ch_summaries = context.get("chapters_summary", [])
        if ch_summaries:
            ch_lines = []
            for ch in ch_summaries:
                ch_lines.append(f"  - {ch.get('title', 'Untitled')}: {ch.get('summary', 'Belum dirangkum')}")
            parts.append(f"Ringkasan Bab:\n" + "\n".join(ch_lines))
        
        if not parts:
            return ""
        
        return "=== KONTEKS TESIS ===\n" + "\n\n".join(parts) + "\n=== AKHIR KONTEKS ==="
    
    
    def _build_history_session(project_id: str, turns: list[dict]) -> dict:
        title = "Chat Agent"
        updated_at = int(time.time() * 1000)
    
        if turns:
            first_user = next((turn for turn in turns if turn.get("role") == "user" and turn.get("content")), None)
            if first_user:
                text = str(first_user["content"]).strip()
                title = text[:50] + ("..." if len(text) > 50 else "")
    
            last_turn = turns[-1]
            timestamp_value = last_turn.get("timestamp")
            if isinstance(timestamp_value, str):
                try:
                    updated_at = int(datetime.fromisoformat(timestamp_value.replace("Z", "+00:00")).timestamp() * 1000)
                except Exception:
                    updated_at = int(time.time() * 1000)
    
        messages = []
        for turn in turns:
            messages.append({
                "role": turn.get("role"),
                "content": turn.get("content", ""),
                "timestamp": turn.get("timestamp"),
            })
    
        return {
            "id": f"project:{project_id}",
            "title": title,
            "updatedAt": updated_at,
            "createdAt": updated_at,
            "messages": messages,
        }
    
    
    def _normalize_diff_payload(diff: dict) -> dict:
        if not isinstance(diff, dict):
            return {}
    
        diff_id = diff.get("diff_id") or diff.get("diffId") or ""
        old_text = diff.get("old_text")
        if old_text is None:
            old_text = diff.get("before", "")
        new_text = diff.get("new_text")
        if new_text is None:
            new_text = diff.get("after", "")
    
        normalized = dict(diff)
        normalized["diffId"] = diff_id
        normalized["diff_id"] = diff_id
        normalized["old_text"] = old_text
        normalized["new_text"] = new_text
        normalized.setdefault("before", old_text)
        normalized.setdefault("after", new_text)
        return normalized
    
    
    def _should_precompute_pruned_context(task: str, context: dict) -> bool:
        task_lc = (task or "").strip().lower()
        if not task_lc:
            return False
        lightweight_messages = {
            "halo", "hai", "hi", "hello", "pagi", "siang", "sore", "malam",
            "halo agent", "hai agent", "hi agent", "hey", "ping",
        }
        if task_lc in lightweight_messages:
            return False
        if len(task_lc.split()) <= 3 and "?" not in task_lc:
            return False
        return bool(
            (context or {}).get("active_paragraphs")
            or (context or {}).get("context_problem")
            or (context or {}).get("context_title")
        )
    
    
    # ──────────────────────────────────────────────
    # NEW: POST /api/summarize-chapter
    # ──────────────────────────────────────────────
    
    @agent_api_bp.route('/api/summarize-chapter', methods=['POST'])
    @login_required
    def summarize_chapter():
        """
        Receives chapter content, generates a concise summary, and saves to Qdrant.
        """
        data = request.json
        user_id = str(current_user.id)
        project_id = data.get("projectId")
        chapter_id = data.get("chapterId")
        content = data.get("content", "")
    
        if not project_id or not chapter_id or not content:
            return jsonify({"error": "Missing project_id, chapter_id or content"}), 400
    
        try:
            import litellm  # pyre-ignore
            from litellm.exceptions import RateLimitError
            
            # Generate summary using a fast model
            summary_prompt = f"Rangkum bab tesis berikut dalam maksimal 100 kata. Fokus pada poin-poin utama saja.\n\nKONTEN:\n{content[:15000]}"
            
            try:
                response = litellm.completion(
                    model="groq/llama-3.1-8b-instant", # Use fast model for summaries
                    messages=[{"role": "user", "content": summary_prompt}],
                    max_tokens=250,
                    timeout=15,
                    temperature=0.3
                )
                summary_text = response.choices[0].message.content.strip()
            except Exception as groq_err:
                logger.warning(f"Groq failed for summarize_chapter: {groq_err}, falling back to Gemini.")
                fallback_api_key = os.environ.get("GEMINI_API_KEY")
                if not fallback_api_key:
                    raise Exception("Rate limit hit and GEMINI_API_KEY not set")
                response = litellm.completion(
                    model="gemini/gemini-2.5-flash",
                    messages=[{"role": "user", "content": summary_prompt}],
                    api_key=fallback_api_key,
                    max_tokens=250,
                    timeout=15,
                    temperature=0.3
                )
                summary_text = response.choices[0].message.content.strip()
    
            # Save to Qdrant
            vdb = QdrantVectorDB()
            shared_mem = SharedMemory(user_id, project_id, vdb, None)
            shared_mem.document.add_or_update_chapter_summary(shared_mem.project_scope, chapter_id, summary_text)
    
            return jsonify({
                "status": "success",
                "summary": summary_text
            }), 200
    
        except Exception as e:
            logger.error(f"Error in summarize_chapter: {e}")
            return jsonify({"error": str(e)}), 500
    
    
    def _build_pruned_context(task: str, context: dict, user_id: str, project_id: str) -> str:
        """
        Builds a context string while keeping it under ~8,000 tokens.
        Priority: 1. Metadata > 2. Active Chapter > 3. Chapter Summaries > 4. Semantic Fragments.
        """
        MAX_TOKENS = 8000
    
        if not user_id:
            raise ValueError("_build_pruned_context requires user_id")
        if not project_id:
            raise ValueError("_build_pruned_context requires project_id")
        
        # Initialize SharedMemory
        try:
            from app.agent.memory_system import QdrantVectorDB, SharedMemory  # type: ignore
            vdb = QdrantVectorDB()
            # Mocking db for SharedMemory since we use Qdrant for active context
            shared_mem = SharedMemory(user_id, project_id, vdb, None)
        except Exception as e:
            logger.warning(f"Could not init SharedMemory: {e}")
            shared_mem = None
    
        parts = []
        
        # 1. Base Project Info (Highest Priority)
        parts.append(f"Judul Tesis: {context.get('context_title', 'Untitled')}")
        parts.append(f"Rumusan Masalah: {context.get('context_problem', '-')}")
        parts.append(f"Metodologi: {context.get('context_method', '-')}")
        
        # 2. Active Paragraphs
        paragraphs = context.get("active_paragraphs", [])
        if paragraphs:
            para_lines = []
            for p in paragraphs:
                para_lines.append(f"  [{p.get('paraId', '?')}] {p.get('content', '')}")
            parts.append("Bab Aktif:\n" + "\n".join(para_lines))
    
        # 3. Chapter Summaries
        if shared_mem:
            try:
                summaries = shared_mem.document.get_all_chapter_summaries(shared_mem.project_scope)
                if summaries:
                    sum_lines = [f"  - {s['chapter_id']}: {s['summary']}" for s in summaries]
                    parts.append("Ringkasan Bab Lain:\n" + "\n".join(sum_lines))
            except Exception as e:
                logger.warning(f"Failed to fetch summaries: {e}")
    
        # 4. Semantic Fragments (Retrieve based on current task)
        if shared_mem:
            try:
                fragments = shared_mem.document.get_relevant_context(task, shared_mem.project_scope, top_k=3)
                if fragments:
                    parts.append(f"Fragmen Relevan dari Bab Lain:\n{fragments}")
            except Exception as e:
                logger.warning(f"Failed semantic retrieval: {e}")
    
        # 5. Token Pruning
        full_context = "=== KONTEKS TESIS ===\n" + "\n\n".join(parts) + "\n=== AKHIR KONTEKS ==="
        
        if count_tokens(full_context) <= MAX_TOKENS:
            return full_context
        
        # If over budget, we prune from fragments first, then summaries, then active chapter
        # Dynamic pruning implementation
        current_tokens = count_tokens(full_context)
        logger.info(f"Context too large ({current_tokens} tokens), pruning...")
        
        # Simple strategy: Keep only top 10 active paragraphs if still too large
        if paragraphs and len(paragraphs) > 10:
            parts[3] = "Bab Aktif (Terpotong):\n" + "\n".join([f"  [{p.get('paraId','?')}] {p.get('content','')}" for p in paragraphs[:10]])
        
        return "=== KONTEKS TESIS (TRUNCATED) ===\n" + "\n\n".join(parts) + "\n=== AKHIR KONTEKS ==="
    
    
    # ──────────────────────────────────────────────
    # REMOVED: POST /api/agent/chat (legacy synchronous endpoint)
    # Was a dead endpoint — no frontend caller.
    # All agent interactions now go through /api/agent/run (SSE).
    # ──────────────────────────────────────────────
    
    
    # ──────────────────────────────────────────────
    # NEW: POST /api/agent/run — SSE Streaming Endpoint
    # ──────────────────────────────────────────────
    
    @agent_api_bp.route('/api/agent/run', methods=['POST', 'OPTIONS'])
    @login_required
    def run_agent_sse():
        """
        SSE streaming endpoint for the AgentPanel.
        Receives a task + thesis context, runs an agentic loop with tool calling,
        and streams events (STEP, TOOL_CALL, TOOL_RESULT, PENDING_DIFF, TEXT_DELTA, DONE, ERROR).
        """
        # Handle CORS preflight
        if request.method == 'OPTIONS':
            resp = Response()
            resp.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            resp.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
            resp.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
            resp.headers['Access-Control-Allow-Credentials'] = 'true'
            return resp, 200
    
        data = request.json
        if not data:
            return jsonify({"error": "Missing JSON body"}), 400
    
        # Gap 11: Use authenticated user ID for user-scoped memory
        user_id = current_user.id
    
        task = data.get("user_message") or data.get("task", "")
        context = dict(data.get("context", {}) or {})
        selected_text = (
            context.get("selected_text")
            or context.get("selectedText")
            or data.get("selected_text")
            or data.get("selectedText")
            or ""
        )
        target_paragraph_key = (
            context.get("target_paragraph_key")
            or context.get("target_key")
            or context.get("targetKey")
            or data.get("target_paragraph_key")
            or data.get("targetKey")
            or ""
        )
        if selected_text:
            context["selected_text"] = selected_text
        if target_paragraph_key:
            context["target_paragraph_key"] = target_paragraph_key
            context["target_key"] = target_paragraph_key
        source = data.get("source") or context.get("source") or "chat"
        requested_intent = data.get("intent") or context.get("intent") or ""
        context["source"] = source
        if requested_intent and requested_intent != "general":
            context["requested_intent"] = requested_intent
        project_id = data.get("projectId", "")
        chapter_id = data.get("chapterId", "")
        messages_history = data.get("messages", [])
        model_key = data.get("model", DEFAULT_MODEL_KEY)
        mode = data.get("mode", "planning")
    
        if not task:
            return jsonify({"error": "Field 'task' is required"}), 400
        if not project_id:
            return jsonify({"error": "Field 'projectId' is required"}), 400
    
        logger.info(f"[AgentRun] user={user_id} project={project_id} task={task[:50]}")
    
        # Resolve LLM model
        primary_model = MODEL_MAP.get(model_key, MODEL_MAP[DEFAULT_MODEL_KEY])
        def generate():
            """SSE generator - delegates execution to SupervisorAgent and streams events."""
            from gevent import spawn  # type: ignore
            from gevent.queue import Queue  # type: ignore
            from app.agent.supervisor import SupervisorAgent  # type: ignore
    
            request_start = time.time()  # S2-5: Request-level timing
    
            # Module-level singleton — avoids re-creating AgentRegistry/Qdrant per request
            if not hasattr(run_agent_sse, '_supervisor'):
                run_agent_sse._supervisor = SupervisorAgent()
            supervisor = run_agent_sse._supervisor
    
            def emit(event_data: dict) -> str:
                return f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
    
            try:
                event_queue = Queue()
                emitted_text_delta = False
                done_sentinel = "__SUPERVISOR_DONE__"
    
                def on_event(event_type: str, data: dict):
                    payload = {"type": event_type}
                    if isinstance(data, dict):
                        payload.update(data)
                    event_queue.put(payload)
    
                runtime_context = dict(context or {})
                runtime_context["projectId"] = project_id
                runtime_context["chapterId"] = chapter_id
                runtime_context["messages_history"] = messages_history[-6:] if messages_history else []
                runtime_context["_llm_model"] = primary_model
                runtime_context["_mode"] = mode
                runtime_context["_skip_semantic_retrieval"] = False
    
                if _should_precompute_pruned_context(task, runtime_context):
                    try:
                        runtime_context["_pruned_context"] = _build_pruned_context(
                            task,
                            runtime_context,
                            str(user_id),
                            project_id,
                        )
                        runtime_context["_skip_semantic_retrieval"] = True
                    except Exception as prune_error:
                        logger.warning(f"[AgentRun] Failed to precompute pruned context: {prune_error}")
    
                worker_state: dict = {}
    
                def worker():
                    try:
                        worker_state["result"] = supervisor.process_request(
                            user_id=str(user_id),
                            message=task,
                            context=runtime_context,
                            on_event=on_event,
                        )
                    except Exception as worker_error:
                        worker_state["error"] = str(worker_error)
                    finally:
                        event_queue.put({"type": done_sentinel})
    
                spawn(worker)
    
                while True:
                    event = event_queue.get()
                    if event.get("type") == done_sentinel:
                        break
    
                    if event.get("type") == "TEXT_DELTA":
                        emitted_text_delta = True
    
                    yield emit(event)
    
                    if event.get("type") == "TOOL_RESULT":
                        result = event.get("result")
                        if isinstance(result, dict) and result.get("diff"):
                            diff_payload = _normalize_diff_payload(result["diff"])
                            yield emit({
                                "type": "paragraph_proposed",
                                "diff": diff_payload,
                                "old_text": diff_payload.get("old_text", ""),
                                "new_text": diff_payload.get("new_text", "")
                            })
                        if isinstance(result, dict):
                            for citation_flag in result.get("citation_flags", []):
                                yield emit({
                                    "type": "CITATION_FLAG",
                                    "citation_flag": citation_flag,
                                })
                            for warning in result.get("warnings", []):
                                yield emit({
                                    "type": "INCOHERENCE_WARNING",
                                    "warning": warning,
                                })
    
                if worker_state.get("error"):
                    yield emit({"type": "ERROR", "message": f"Unexpected error: {worker_state['error']}"})
                    return
    
                final_text = worker_state.get("result")
                if final_text and not emitted_text_delta:
                    yield emit({"type": "TEXT_DELTA", "delta": str(final_text)})
    
                # S2-5: Request-level latency observability
                request_duration_ms = int((time.time() - request_start) * 1000)
                logger.info(
                    f"[Observability] SSE request user={user_id} "
                    f"project={project_id} "
                    f"duration_ms={request_duration_ms} "
                    f"task={task[:50]}"
                )
    
                yield emit({"type": "DONE"})
    
            except Exception as e:
                logger.error(f"[AgentRun] Unexpected error: {e}")
                import traceback
                traceback.print_exc()
                yield emit({"type": "ERROR", "message": f"Unexpected error: {str(e)}"})
    
        response = Response(
            stream_with_context(generate()),
            mimetype='text/event-stream',
            headers={
                'Cache-Control': 'no-cache',
                'Connection': 'keep-alive',
                'X-Accel-Buffering': 'no',  # Disable nginx buffering
                'Access-Control-Allow-Origin': request.headers.get('Origin', '*'),
                'Access-Control-Allow-Credentials': 'true',
            }
        )
        return response
    
    
    @agent_api_bp.route('/api/agent/history/<project_id>', methods=['GET'])
    @login_required
    def get_agent_history(project_id):
        user_id = str(current_user.id)
        memory = SharedMemory(user_id, project_id, QdrantVectorDB(), FirestoreDocumentDB())
        turns = memory.conversation.get_full_history()
        session = _build_history_session(project_id, turns)
        return jsonify({
            "status": "success",
            "sessions": [session] if session.get("messages") else [],
            "session": session,
        }), 200
    
    
    @agent_api_bp.route('/api/agent/history/<project_id>', methods=['PUT'])
    @login_required
    def sync_agent_history(project_id):
        data = request.json or {}
        messages = data.get("messages", [])
        user_id = str(current_user.id)
        memory = SharedMemory(user_id, project_id, QdrantVectorDB(), FirestoreDocumentDB())
        memory.conversation.replace_from_messages(messages)
        session = _build_history_session(project_id, memory.conversation.get_full_history())
        return jsonify({"status": "success", "session": session}), 200
    
    
    @agent_api_bp.route('/api/agent/history/<project_id>', methods=['DELETE'])
    @login_required
    def clear_agent_history(project_id):
        user_id = str(current_user.id)
        memory = SharedMemory(user_id, project_id, QdrantVectorDB(), FirestoreDocumentDB())
        memory.flush_session()
        return jsonify({"status": "success"}), 200
    
    

    Comments

    More Rules

    View all

    Sleepycat Seo Platform Gemini Rules

    A
    acovrp

    LITE IDE Gemini Rules

    H
    Heyz24

    Shadowtagai Monorepo V2 Gemini Rules

    S
    ShadowTag-v2

    WoKFlow Gemini Rules

    H
    howyongheng0313

    MetaPrompter Gemini Rules

    U
    ummerr

    Taskora Gemini Rules

    K
    kabilesh-c

    Stay up to date

    Get the latest Gemini prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for Gemini and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.

    Neura Market

    Custom AI Systems & Services

    Our team of experienced AI builders will help build custom AI systems, workflows, and solutions for your business.

    Request custom work

    Ready-made automations for this

    Workflows from the Neura Market marketplace related to this Gemini resource

    • Process Multiple Media Files in Telegram with Gemini AI & PostgreSQL Databasen8n · $24.99 · Related topic
    • Convert Telegram Messages to Professional LinkedIn Posts with Gemini AI & Approval Workflown8n · $24.99 · Related topic
    • Create Custom PDF Documents from Templates with Gemini & Google Driven8n · $24.99 · Related topic
    • Kubernetes RCA and Alerting Using Gemini, Loki, Prometheus, Slackn8n · $24.99 · Related topic
    Browse all workflows