[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EMBXYG0Y5RZPPRQQ83TFE4":3,"skill-same:01M6EMBXYG0Y5RZPPRQQ83TFE4":84},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":6,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M6EMBXYG0Y5RZPPRQQ83TFE4","context7-integration",null,"Use when integrating Context7 (knowledge\u002Fcontext store) for document ingestion,\nsemantic search, or scoped context retrieval.\nTriggers for: uploading documents, searching knowledge base, filtering by role\u002Ftenant,\nor providing AI with document-grounded context.\nNOT for: general database queries, file storage without context semantics, or non-document content.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fcontext7-integration\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"context7 integration","integration","context7","use","Document Ingestion","database queries","triggers","knowledge base","document","general","ingestion","storage","filtering","context","knowledge","file","search","database","semantic search","documents","2026-11-10 10:12:30",false,"---\nname: context7-integration\ndescription: |\n  Use when integrating Context7 (knowledge\u002Fcontext store) for document ingestion,\n  semantic search, or scoped context retrieval.\n  Triggers for: uploading documents, searching knowledge base, filtering by role\u002Ftenant,\n  or providing AI with document-grounded context.\n  NOT for: general database queries, file storage without context semantics, or non-document content.\n---\n\n# Context7 Integration Skill\n\nExpert integration of Context7 for document ingestion, semantic search, and role-scoped context retrieval in ERP applications.\n\n## Quick Reference\n\n| Task | Method\u002FEndpoint |\n|------|-----------------|\n| Ingest document | `context7_client.ingest_document()` |\n| Batch ingest | `context7_client.ingest_batch()` |\n| Search context | `context7_client.search()` |\n| Get document | `context7_client.get_document()` |\n| Delete document | `context7_client.delete_document()` |\n\n## Project Structure\n\n```\nbackend\u002F\n├── app\u002F\n│   ├── services\u002F\n│   │   └── context7_client.py    # Core Context7 client\n│   ├── api\u002F\n│   │   └── knowledge\u002F\n│   │       └── routes.py         # Knowledge API endpoints\n│   └── schemas\u002F\n│       └── knowledge.py          # Pydantic schemas\nfrontend\u002F\n├── hooks\u002F\n│   └── useContext7Search.ts     # Search hook\n└── components\u002F\n    └── knowledge\u002F\n        └── ContextSearch.tsx    # Search component\ndocs\u002F\n├── policies\u002F                     # Source documents\n├── faq\u002F                          # FAQ documents\n└── procedures\u002F                   # Procedure documents\n```\n\n## Context7 Client\n\n### Core Client Class\n\n```python\n# backend\u002Fapp\u002Fservices\u002Fcontext7_client.py\nimport os\nfrom typing import Optional\nfrom pydantic import BaseModel\nfrom enum import Enum\nfrom datetime import datetime\n\n\nclass DocumentType(str, Enum):\n    MARKDOWN = \"markdown\"\n    PDF = \"pdf\"\n    HTML = \"html\"\n    TEXT = \"text\"\n\n\nclass DocumentMetadata(BaseModel):\n    \"\"\"Metadata for context documents.\"\"\"\n    title: str\n    description: Optional[str] = None\n    # Role-based access\n    allowed_roles: list[str] = []  # Empty = all roles\n    # Organization scope\n    school_id: Optional[str] = None\n    # Content categorization\n    module: str  # e.g., \"fees\", \"attendance\", \"policies\"\n    category: Optional[str] = None  # e.g., \"faq\", \"procedure\", \"policy\"\n    # Language\n    language: str = \"en\"\n    # Versioning\n    version: str = \"1.0\"\n    effective_date: Optional[datetime] = None\n    expiry_date: Optional[datetime] = None\n    # Source tracking\n    source_file: Optional[str] = None\n    source_url: Optional[str] = None\n\n\nclass ContextDocument(BaseModel):\n    \"\"\"Document in Context7.\"\"\"\n    id: str\n    content: str\n    metadata: DocumentMetadata\n    chunk_id: Optional[str] = None\n    similarity_score: Optional[float] = None\n\n\nclass Context7Client:\n    \"\"\"Client for Context7 knowledge store.\"\"\"\n\n    def __init__(\n        self,\n        api_key: Optional[str] = None,\n        base_url: Optional[str] = None,\n        default_namespace: str = \"default\",\n    ):\n        self.api_key = api_key or os.getenv(\"CONTEXT7_API_KEY\")\n        self.base_url = base_url or os.getenv(\"CONTEXT7_API_URL\", \"https:\u002F\u002Fapi.context7.com\")\n        self.default_namespace = default_namespace\n        self._session = None\n\n    def _get_session(self):\n        \"\"\"Get or create requests session.\"\"\"\n        if not self._session:\n            import requests\n            self._session = requests.Session()\n            if self.api_key:\n                self._session.headers.update({\"Authorization\": f\"Bearer {self.api_key}\"})\n        return self._session\n\n    def _request(\n        self,\n        method: str,\n        endpoint: str,\n        **kwargs\n    ) -> dict:\n        \"\"\"Make API request to Context7.\"\"\"\n        session = self._get_session()\n        response = session.request(\n            method,\n            f\"{self.base_url}{endpoint}\",\n            **kwargs\n        )\n        response.raise_for_status()\n        return response.json()\n\n    # === INGESTION ===\n\n    def ingest_document(\n        self,\n        content: str,\n        metadata: DocumentMetadata,\n        namespace: Optional[str] = None,\n        document_id: Optional[str] = None,\n    ) -> dict:\n        \"\"\"\n        Ingest a single document into Context7.\n\n        Args:\n            content: Document content (markdown, HTML, or text)\n            metadata: Document metadata with tags and access control\n            namespace: Optional namespace (defaults to default_namespace)\n            document_id: Optional document ID for idempotent updates\n\n        Returns:\n            Ingestion result with document ID\n        \"\"\"\n        payload = {\n            \"content\": content,\n            \"metadata\": metadata.model_dump(),\n            \"document_id\": document_id,\n            \"namespace\": namespace or self.default_namespace,\n        }\n\n        return self._request(\"POST\", \"\u002Fv1\u002Fdocuments\", json=payload)\n\n    def ingest_batch(\n        self,\n        documents: list[tuple[str, DocumentMetadata]],\n        namespace: Optional[str] = None,\n        batch_size: int = 10,\n    ) -> dict:\n        \"\"\"\n        Ingest multiple documents in batches.\n\n        Args:\n            documents: List of (content, metadata) tuples\n            namespace: Optional namespace\n            batch_size: Number of documents per batch\n\n        Returns:\n            Batch ingestion result with success\u002Ffailure counts\n        \"\"\"\n        results = {\"successful\": 0, \"failed\": 0, \"documents\": []}\n        namespace = namespace or self.default_namespace\n\n        for i in range(0, len(documents), batch_size):\n            batch = documents[i:i + batch_size]\n            batch_payload = [\n                {\n                    \"content\": content,\n                    \"metadata\": metadata.model_dump(),\n                    \"namespace\": namespace,\n                }\n                for content, metadata in batch\n            ]\n\n            try:\n                response = self._request(\n                    \"POST\",\n                    \"\u002Fv1\u002Fdocuments\u002Fbatch\",\n                    json={\"documents\": batch_payload}\n                )\n                results[\"successful\"] += len(batch)\n                results[\"documents\"].extend(response.get(\"documents\", []))\n            except Exception as e:\n                results[\"failed\"] += len(batch)\n                # Log failed batch for retry\n\n        return results\n\n    def ingest_from_file(\n        self,\n        file_path: str,\n        metadata: DocumentMetadata,\n        namespace: Optional[str] = None,\n    ) -> dict:\n        \"\"\"\n        Ingest a document from a file.\n\n        Args:\n            file_path: Path to file (markdown, PDF, or HTML)\n            metadata: Document metadata\n            namespace: Optional namespace\n\n        Returns:\n            Ingestion result\n        \"\"\"\n        # Determine document type from extension\n        ext = os.path.splitext(file_path)[1].lower()\n\n        if ext == \".md\":\n            with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                content = f.read()\n            doc_type = DocumentType.MARKDOWN\n        elif ext == \".pdf\":\n            content = self._extract_pdf_text(file_path)\n            doc_type = DocumentType.PDF\n        elif ext in [\".html\", \".htm\"]:\n            with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                content = f.read()\n            content = self._strip_html(content)\n            doc_type = DocumentType.HTML\n        else:\n            # Default to text\n            with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                content = f.read()\n            doc_type = DocumentType.TEXT\n\n        # Update metadata with source file\n        metadata.source_file = file_path\n\n        return self.ingest_document(content, metadata, namespace)\n\n    def _extract_pdf_text(self, file_path: str) -> str:\n        \"\"\"Extract text from PDF file.\"\"\"\n        try:\n            import PyPDF2\n            with open(file_path, \"rb\") as f:\n                reader = PyPDF2.PdfReader(f)\n                text = \"\\n\".join(page.extract_text() for page in reader.pages)\n            return text\n        except ImportError:\n            raise ImportError(\"PyPDF2 required for PDF ingestion: pip install PyPDF2\")\n\n    def _strip_html(self, html: str) -> str:\n        \"\"\"Strip HTML tags from content.\"\"\"\n        import re\n        clean = re.compile(\"\u003C.*?>\")\n        return re.sub(clean, \"\", html)\n\n    # === RETRIEVAL ===\n\n    def search(\n        self,\n        query: str,\n        namespace: Optional[str] = None,\n        filters: Optional[dict] = None,\n        max_chunks: int = 5,\n        min_similarity: float = 0.7,\n        user_role: Optional[str] = None,\n        school_id: Optional[str] = None,\n    ) -> list[ContextDocument]:\n        \"\"\"\n        Search for relevant context documents.\n\n        Args:\n            query: Search query (semantic search)\n            namespace: Namespace to search in\n            filters: Additional metadata filters\n            max_chunks: Maximum number of chunks to return\n            min_similarity: Minimum similarity score threshold\n            user_role: User's role for access control\n            school_id: User's school ID for multi-tenancy\n\n        Returns:\n            List of relevant document chunks\n        \"\"\"\n        # Build search payload with access control\n        payload = {\n            \"query\": query,\n            \"namespace\": namespace or self.default_namespace,\n            \"max_chunks\": max_chunks,\n            \"min_similarity\": min_similarity,\n            \"filters\": filters or {},\n        }\n\n        # Add role-based filtering\n        if user_role:\n            payload[\"filters\"][\"allowed_roles\"] = [user_role, \"all\"]\n\n        # Add tenant filtering\n        if school_id:\n            payload[\"filters\"][\"school_id\"] = school_id\n\n        response = self._request(\"POST\", \"\u002Fv1\u002Fsearch\", json=payload)\n\n        return [\n            ContextDocument(\n                id=doc.get(\"id\"),\n                content=doc.get(\"content\", \"\"),\n                metadata=DocumentMetadata(**doc.get(\"metadata\", {})),\n                chunk_id=doc.get(\"chunk_id\"),\n                similarity_score=doc.get(\"similarity_score\"),\n            )\n            for doc in response.get(\"documents\", [])\n        ]\n\n    def get_document(\n        self,\n        document_id: str,\n        namespace: Optional[str] = None,\n    ) -> Optional[ContextDocument]:\n        \"\"\"\n        Get a specific document by ID.\n\n        Args:\n            document_id: Document ID\n            namespace: Namespace\n\n        Returns:\n            Document or None if not found\n        \"\"\"\n        try:\n            response = self._request(\n                \"GET\",\n                f\"\u002Fv1\u002Fdocuments\u002F{document_id}\",\n                params={\"namespace\": namespace or self.default_namespace}\n            )\n            return ContextDocument(\n                id=response.get(\"id\"),\n                content=response.get(\"content\", \"\"),\n                metadata=DocumentMetadata(**response.get(\"metadata\", {})),\n            )\n        except Exception:\n            return None\n\n    def delete_document(\n        self,\n        document_id: str,\n        namespace: Optional[str] = None,\n    ) -> bool:\n        \"\"\"\n        Delete a document from Context7.\n\n        Args:\n            document_id: Document ID\n            namespace: Namespace\n\n        Returns:\n            True if deleted successfully\n        \"\"\"\n        try:\n            self._request(\n                \"DELETE\",\n                f\"\u002Fv1\u002Fdocuments\u002F{document_id}\",\n                params={\"namespace\": namespace or self.default_namespace}\n            )\n            return True\n        except Exception:\n            return False\n\n    # === MANAGEMENT ===\n\n    def list_documents(\n        self,\n        namespace: Optional[str] = None,\n        module: Optional[str] = None,\n        limit: int = 100,\n    ) -> list[dict]:\n        \"\"\"\n        List documents in a namespace.\n\n        Args:\n            namespace: Namespace to list\n            module: Filter by module\n            limit: Maximum number of results\n\n        Returns:\n            List of document summaries\n        \"\"\"\n        params = {\n            \"namespace\": namespace or self.default_namespace,\n            \"limit\": limit,\n        }\n        if module:\n            params[\"module\"] = module\n\n        response = self._request(\"GET\", \"\u002Fv1\u002Fdocuments\", params=params)\n        return response.get(\"documents\", [])\n\n    def get_stats(self, namespace: Optional[str] = None) -> dict:\n        \"\"\"Get statistics for a namespace.\"\"\"\n        response = self._request(\n            \"GET\",\n            \"\u002Fv1\u002Fstats\",\n            params={\"namespace\": namespace or self.default_namespace}\n        )\n        return response\n\n\n# Singleton instance\n_context7_client: Optional[Context7Client] = None\n\n\ndef get_context7_client() -> Context7Client:\n    \"\"\"Get or create Context7 client singleton.\"\"\"\n    global _context7_client\n    if _context7_client is None:\n        _context7_client = Context7Client()\n    return _context7_client\n```\n\n## Context Shaping Utilities\n\n```python\n# backend\u002Fapp\u002Fservices\u002Fcontext_shaper.py\nfrom typing import list\nfrom backend.app.services.context7_client import ContextDocument\n\n\nclass ContextShaper:\n    \"\"\"Shape and format retrieved context for AI prompts.\"\"\"\n\n    MAX_TOKENS = 4000  # Reserve space for prompt\n    CHUNK_HEADER = \"### Source: {title}\"\n    FOOTER = \"\\n\\n---\\n*Source: {source}*\"\n\n    def shape_for_prompt(\n        self,\n        documents: list[ContextDocument],\n        query: str,\n        max_chunks: int = 5,\n        include_sources: bool = True,\n    ) -> str:\n        \"\"\"\n        Shape retrieved documents into prompt-safe format.\n\n        Args:\n            documents: Retrieved document chunks\n            query: Original search query\n            max_chunks: Maximum chunks to include\n            include_sources: Include source citations\n\n        Returns:\n            Formatted context string\n        \"\"\"\n        chunks = documents[:max_chunks]\n\n        sections = []\n        for i, doc in enumerate(chunks):\n            header = f\"## Chunk {i + 1}\"\n            if doc.metadata.title:\n                header += f\": {doc.metadata.title}\"\n\n            section = header\n            section += f\"\\n\\n{self._format_content(doc.content)}\"\n\n            if include_sources and doc.metadata.source_file:\n                section += self.FOOTER.format(source=doc.metadata.source_file)\n\n            sections.append(section)\n\n        context = \"\\n\\n\".join(sections)\n\n        # Ensure context fits in token limit\n        context = self._truncate_to_token_limit(context, self.MAX_TOKENS)\n\n        return context\n\n    def _format_content(self, content: str) -> str:\n        \"\"\"Format content for readability.\"\"\"\n        # Normalize whitespace\n        lines = content.split(\"\\n\")\n        lines = [line.strip() for line in lines if line.strip()]\n        return \"\\n\".join(lines)\n\n    def _truncate_to_token_limit(self, text: str, max_tokens: int) -> str:\n        \"\"\"Truncate text to fit within token limit.\"\"\"\n        # Rough estimate: 4 characters per token\n        max_chars = max_tokens * 4\n\n        if len(text) \u003C= max_chars:\n            return text\n\n        # Truncate and add note\n        truncated = text[:max_chars - 50]\n        truncated = truncated.rsplit(\"\\n\", 1)[0]  # Don't cut mid-line\n        truncated += \"\\n\\n*... (context truncated for length)*\"\n\n        return truncated\n\n    def format_for_chat(\n        self,\n        documents: list[ContextDocument],\n        user_role: str,\n    ) -> str:\n        \"\"\"\n        Format context for chat widget display.\n\n        Args:\n            documents: Retrieved documents\n            user_role: User's role for messaging\n\n        Returns:\n            User-friendly formatted context\n        \"\"\"\n        if not documents:\n            return \"No relevant information found.\"\n\n        formatted = []\n        for doc in documents:\n            if doc.metadata.title:\n                formatted.append(f\"**{doc.metadata.title}**\")\n            formatted.append(doc.content[:500])  # Limit per chunk\n            formatted.append(\"\")\n\n        return \"\\n\".join(formatted)\n\n\n# Singleton\ncontext_shaper = ContextShaper()\n```\n\n## API Routes\n\n```python\n# backend\u002Fapp\u002Fapi\u002Fknowledge\u002Froutes.py\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom typing import Optional\nfrom pydantic import BaseModel\n\nfrom app.services.context7_client import (\n    get_context7_client,\n    DocumentMetadata,\n    Context7Client,\n)\nfrom app.services.context_shaper import get_context_shaper, ContextShaper\nfrom app.auth.jwt import get_current_user\n\n\nrouter = APIRouter(prefix=\"\u002Fknowledge\", tags=[\"knowledge\"])\n\n\nclass IngestRequest(BaseModel):\n    \"\"\"Request to ingest a document.\"\"\"\n    content: str\n    title: str\n    description: Optional[str] = None\n    module: str\n    category: Optional[str] = None\n    allowed_roles: list[str] = []\n    school_id: Optional[str] = None\n\n\nclass SearchRequest(BaseModel):\n    \"\"\"Request to search knowledge base.\"\"\"\n    query: str\n    module: Optional[str] = None\n    max_chunks: int = 5\n    min_similarity: float = 0.7\n\n\nclass SearchResponse(BaseModel):\n    \"\"\"Search response with shaped context.\"\"\"\n    documents: list[dict]\n    context: str  # Shaped for prompt\n\n\n@router.post(\"\u002Fingest\")\nasync def ingest_document(\n    request: IngestRequest,\n    current_user = Depends(get_current_user),\n    client: Context7Client = Depends(get_context7_client),\n) -> dict:\n    \"\"\"\n    Ingest a document into the knowledge base.\n\n    Requires admin or content-manager role.\n    \"\"\"\n    # Check permissions\n    if \"admin\" not in current_user.roles and \"content-manager\" not in current_user.roles:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail=\"Insufficient permissions to ingest documents\",\n        )\n\n    # Build metadata\n    metadata = DocumentMetadata(\n        title=request.title,\n        description=request.description,\n        allowed_roles=request.allowed_roles or [\"all\"],\n        school_id=request.school_id or current_user.school_id,\n        module=request.module,\n        category=request.category,\n    )\n\n    # Ingest\n    result = client.ingest_document(\n        content=request.content,\n        metadata=metadata,\n    )\n\n    return {\"status\": \"ingested\", \"document_id\": result.get(\"id\")}\n\n\n@router.post(\"\u002Fsearch\", response_model=SearchResponse)\nasync def search_knowledge(\n    request: SearchRequest,\n    current_user = Depends(get_current_user),\n    client: Context7Client = Depends(get_context7_client),\n    shaper: ContextShaper = Depends(get_context_shaper),\n) -> SearchResponse:\n    \"\"\"\n    Search the knowledge base.\n\n    Returns shaped context suitable for AI prompts.\n    \"\"\"\n    # Search with role and tenant filtering\n    documents = client.search(\n        query=request.query,\n        filters={\"module\": request.module} if request.module else {},\n        max_chunks=request.max_chunks,\n        min_similarity=request.min_similarity,\n        user_role=current_user.role,\n        school_id=current_user.school_id,\n    )\n\n    # Shape for prompt\n    context = shaper.shape_for_prompt(\n        documents=documents,\n        query=request.query,\n        max_chunks=request.max_chunks,\n    )\n\n    return SearchResponse(\n        documents=[\n            {\n                \"id\": doc.id,\n                \"title\": doc.metadata.title,\n                \"content\": doc.content[:200],\n                \"similarity\": doc.similarity_score,\n            }\n            for doc in documents\n        ],\n        context=context,\n    )\n\n\n@router.get(\"\u002Fmodules\")\nasync def list_modules(\n    current_user = Depends(get_current_user),\n    client: Context7Client = Depends(get_context7_client),\n) -> dict:\n    \"\"\"List available knowledge modules.\"\"\"\n    documents = client.list_documents(\n        limit=1000,\n    )\n\n    modules = set()\n    for doc in documents:\n        if doc.get(\"metadata\", {}).get(\"school_id\") in [None, current_user.school_id]:\n            modules.add(doc.get(\"metadata\", {}).get(\"module\"))\n\n    return {\"modules\": sorted(modules)}\n```\n\n## Frontend Hook\n\n```typescript\n\u002F\u002F frontend\u002Fhooks\u002FuseContext7Search.ts\nimport { useState, useCallback } from \"react\";\n\ninterface SearchResult {\n  id: string;\n  title: string;\n  content: string;\n  similarity: number;\n}\n\ninterface SearchOptions {\n  module?: string;\n  maxChunks?: number;\n}\n\nexport function useContext7Search() {\n  const [results, setResults] = useState\u003CSearchResult[]>([]);\n  const [context, setContext] = useState\u003Cstring>(\"\");\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState\u003Cstring | null>(null);\n\n  const search = useCallback(async (\n    query: string,\n    options: SearchOptions = {}\n  ) => {\n    setLoading(true);\n    setError(null);\n\n    try {\n      const response = await fetch(\"\u002Fapi\u002Fv1\u002Fknowledge\u002Fsearch\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application\u002Fjson\",\n        },\n        body: JSON.stringify({\n          query,\n          module: options.module,\n          max_chunks: options.maxChunks || 5,\n        }),\n      });\n\n      if (!response.ok) {\n        throw new Error(\"Search failed\");\n      }\n\n      const data = await response.json();\n\n      setResults(data.documents);\n      setContext(data.context);\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Search failed\");\n      setResults([]);\n      setContext(\"\");\n    } finally {\n      setLoading(false);\n    }\n  }, []);\n\n  const clear = useCallback(() => {\n    setResults([]);\n    setContext(\"\");\n    setError(null);\n  }, []);\n\n  return {\n    search,\n    clear,\n    results,\n    context,\n    loading,\n    error,\n  };\n}\n```\n\n## Document Organization\n\n```\ndocs\u002F\n├── policies\u002F\n│   ├── attendance-policy.md\n│   ├── fee-refund-policy.md\n│   └── grading-policy.md\n├── procedures\u002F\n│   ├── student-registration.md\n│   ├── fee-payment.md\n│   └── transcript-request.md\n├── faq\u002F\n│   ├── fees-faq.md\n│   ├── attendance-faq.md\n│   └── grades-faq.md\n└── handbooks\u002F\n    ├── student-handbook.md\n    └── parent-handbook.md\n```\n\n### Metadata Examples\n\n```python\n# Fee policy document\nDocumentMetadata(\n    title=\"Fee Refund Policy\",\n    description=\"Guidelines for fee refunds and cancellations\",\n    module=\"fees\",\n    category=\"policy\",\n    allowed_roles=[\"admin\", \"accountant\", \"parent\", \"student\"],\n    school_id=\"school_001\",\n    language=\"en\",\n    source_file=\"docs\u002Fpolicies\u002Ffee-refund-policy.md\",\n)\n\n# Student FAQ\nDocumentMetadata(\n    title=\"Fee Payment FAQ\",\n    description=\"Common questions about fee payment\",\n    module=\"fees\",\n    category=\"faq\",\n    allowed_roles=[\"student\", \"parent\"],\n    school_id=\"school_001\",\n    language=\"en\",\n    source_file=\"docs\u002Ffaq\u002Ffees-faq.md\",\n)\n\n# Staff procedure\nDocumentMetadata(\n    title=\"Student Registration Procedure\",\n    description=\"Step-by-step guide for registering new students\",\n    module=\"registration\",\n    category=\"procedure\",\n    allowed_roles=[\"admin\", \"registrar\"],\n    school_id=\"school_001\",\n    language=\"en\",\n    source_file=\"docs\u002Fprocedures\u002Fstudent-registration.md\",\n)\n```\n\n## Quality Checklist\n\n- [ ] **No PII ingestion**: Sensitive documents reviewed before upload\n- [ ] **Query bounds**: max_chunks, min_similarity prevent excessive results\n- [ ] **Error handling**: Graceful fallback when context unavailable\n- [ ] **Source attribution**: Clear indication when answers are document-based\n- [ ] **Multi-tenancy**: school_id filtering prevents cross-tenant access\n- [ ] **Role filtering**: allowed_roles field controls access\n- [ ] **Token limits**: Context truncation prevents prompt overflow\n\n## Integration Points\n\n| Skill | Integration |\n|-------|-------------|\n| `@jwt-auth` | Extract role and school_id from JWT for access control |\n| `@api-client` | API calls for ingest\u002Fsearch endpoints |\n| `@chatkit-widget` | Provide context for AI-powered chat |\n| `@fastapi-app` | Register knowledge API routes |\n| `@error-handling` | Handle context retrieval errors gracefully |\n\n## Multi-Tenancy\n\n```python\n# Per-school namespace isolation\nclass Context7Client:\n    # ...\n\n    def search(self, query: str, school_id: str, **kwargs) -> list[ContextDocument]:\n        # Always filter by school_id\n        return super().search(\n            query,\n            school_id=school_id,\n            filters={\"school_id\": school_id},\n            **kwargs\n        )\n\n    def ingest_document(\n        self,\n        content: str,\n        metadata: DocumentMetadata,\n        school_id: str,\n        **kwargs\n    ) -> dict:\n        # Always set school_id on metadata\n        metadata.school_id = school_id\n        return super().ingest_document(content, metadata, **kwargs)\n```\n\n## Batch Ingestion Script\n\n```python\n# scripts\u002Fingest_docs.py\n#!\u002Fusr\u002Fbin\u002Fenv python3\n\"\"\"Batch ingest documentation into Context7.\"\"\"\nimport os\nimport sys\nfrom pathlib import Path\n\n# Add project to path\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom app.services.context7_client import Context7Client, DocumentMetadata\n\n\ndef ingest_directory(\n    dir_path: str,\n    module: str,\n    category: str,\n    school_id: str,\n    allowed_roles: list[str],\n):\n    \"\"\"Ingest all documents in a directory.\"\"\"\n    client = Context7Client()\n    dir_path = Path(dir_path)\n\n    for file_path in dir_path.rglob(\"*.md\"):\n        print(f\"Ingesting: {file_path}\")\n\n        metadata = DocumentMetadata(\n            title=file_path.stem.replace(\"-\", \" \").title(),\n            module=module,\n            category=category,\n            school_id=school_id,\n            allowed_roles=allowed_roles,\n            source_file=str(file_path),\n        )\n\n        try:\n            client.ingest_from_file(str(file_path), metadata)\n            print(f\"  ✓ Ingested\")\n        except Exception as e:\n            print(f\"  ✗ Failed: {e}\")\n\n\nif __name__ == \"__main__\":\n    import argparse\n\n    parser = argparse.ArgumentParser(description=\"Batch ingest documents\")\n    parser.add_argument(\"--dir\", required=True, help=\"Directory to ingest\")\n    parser.add_argument(\"--module\", required=True, help=\"Document module\")\n    parser.add_argument(\"--category\", required=True, help=\"Document category\")\n    parser.add_argument(\"--school-id\", required=True, help=\"School ID\")\n    parser.add_argument(\"--roles\", default=\"all\", help=\"Comma-separated allowed roles\")\n\n    args = parser.parse_args()\n\n    ingest_directory(\n        args.dir,\n        args.module,\n        args.category,\n        args.school_id,\n        args.roles.split(\",\"),\n    )\n```\n\n## Error Handling\n\n```python\n# backend\u002Fapp\u002Fservices\u002Fcontext7_client.py\n\nclass Context7Error(Exception):\n    \"\"\"Base exception for Context7 errors.\"\"\"\n    pass\n\n\nclass Context7SearchError(Context7Error):\n    \"\"\"Error during context search.\"\"\"\n    pass\n\n\nclass Context7IngestError(Context7Error):\n    \"\"\"Error during document ingestion.\"\"\"\n    pass\n\n\n# Usage in search\ndef search(self, *args, **kwargs) -> list[ContextDocument]:\n    try:\n        return self._search_impl(*args, **kwargs)\n    except Exception as e:\n        raise Context7SearchError(f\"Search failed: {e}\") from e\n\n\n# Frontend fallback\nfunction useContext7Search() {\n  const { search, results, loading, error } = useContext7Search();\n\n  \u002F\u002F Fallback to general response if context fails\n  const handleSearch = async (query: string) => {\n    try {\n      await search(query);\n    } catch {\n      \u002F\u002F Use generic response\n      setResults([]);\n      setContext(\"\");\n    }\n  };\n\n  return { search: handleSearch, results, loading, error };\n}\n```\n","# Context7 Integration Skill\n\nExpert integration of Context7 for document ingestion, semantic search, and role-scoped context retrieval in ERP applications.\n\n## Quick Reference\n\n| Task | Method\u002FEndpoint |\n|------|-----------------|\n| Ingest document | `context7_client.ingest_document()` |\n| Batch ingest | `context7_client.ingest_batch()` |\n| Search context | `context7_client.search()` |\n| Get document | `context7_client.get_document()` |\n| Delete document | `context7_client.delete_document()` |\n\n## Project Structure\n\n```\nbackend\u002F\n├── app\u002F\n│   ├── services\u002F\n│   │   └── context7_client.py    # Core Context7 client\n│   ├── api\u002F\n│   │   └── knowledge\u002F\n│   │       └── routes.py         # Knowledge API endpoints\n│   └── schemas\u002F\n│       └── knowledge.py          # Pydantic schemas\nfrontend\u002F\n├── hooks\u002F\n│   └── useContext7Search.ts     # Search hook\n└── components\u002F\n    └── knowledge\u002F\n        └── ContextSearch.tsx    # Search component\ndocs\u002F\n├── policies\u002F                     # Source documents\n├── faq\u002F                          # FAQ documents\n└── procedures\u002F                   # Procedure documents\n```\n\n## Context7 Client\n\n### Core Client Class\n\n```python\n# backend\u002Fapp\u002Fservices\u002Fcontext7_client.py\nimport os\nfrom typing import Optional\nfrom pydantic import BaseModel\nfrom enum import Enum\nfrom datetime import datetime\n\n\nclass DocumentType(str, Enum):\n    MARKDOWN = \"markdown\"\n    PDF = \"pdf\"\n    HTML = \"html\"\n    TEXT = \"text\"\n\n\nclass DocumentMetadata(BaseModel):\n    \"\"\"Metadata for context documents.\"\"\"\n    title: str\n    description: Optional[str] = None\n    # Role-based access\n    allowed_roles: list[str] = []  # Empty = all roles\n    # Organization scope\n    school_id: Optional[str] = None\n    # Content categorization\n    module: str  # e.g., \"fees\", \"attendance\", \"policies\"\n    category: Optional[str] = None  # e.g., \"faq\", \"procedure\", \"policy\"\n    # Language\n    language: str = \"en\"\n    # Versioning\n    version: str = \"1.0\"\n    effective_date: Optional[datetime] = None\n    expiry_date: Optional[datetime] = None\n    # Source tracking\n    source_file: Optional[str] = None\n    source_url: Optional[str] = None\n\n\nclass ContextDocument(BaseModel):\n    \"\"\"Document in Context7.\"\"\"\n    id: str\n    content: str\n    metadata: DocumentMetadata\n    chunk_id: Optional[str] = None\n    similarity_score: Optional[float] = None\n\n\nclass Context7Client:\n    \"\"\"Client for Context7 knowledge store.\"\"\"\n\n    def __init__(\n        self,\n        api_key: Optional[str] = None,\n        base_url: Optional[str] = None,\n        default_namespace: str = \"default\",\n    ):\n        self.api_key = api_key or os.getenv(\"CONTEXT7_API_KEY\")\n        self.base_url = base_url or os.getenv(\"CONTEXT7_API_URL\", \"https:\u002F\u002Fapi.context7.com\")\n        self.default_namespace = default_namespace\n        self._session = None\n\n    def _get_session(self):\n        \"\"\"Get or create requests session.\"\"\"\n        if not self._session:\n            import requests\n            self._session = requests.Session()\n            if self.api_key:\n                self._session.headers.update({\"Authorization\": f\"Bearer {self.api_key}\"})\n        return self._session\n\n    def _request(\n        self,\n        method: str,\n        endpoint: str,\n        **kwargs\n    ) -> dict:\n        \"\"\"Make API request to Context7.\"\"\"\n        session = self._get_session()\n        response = session.request(\n            method,\n            f\"{self.base_url}{endpoint}\",\n            **kwargs\n        )\n        response.raise_for_status()\n        return response.json()\n\n    # === INGESTION ===\n\n    def ingest_document(\n        self,\n        content: str,\n        metadata: DocumentMetadata,\n        namespace: Optional[str] = None,\n        document_id: Optional[str] = None,\n    ) -> dict:\n        \"\"\"\n        Ingest a single document into Context7.\n\n        Args:\n            content: Document content (markdown, HTML, or text)\n            metadata: Document metadata with tags and access control\n            namespace: Optional namespace (defaults to default_namespace)\n            document_id: Optional document ID for idempotent updates\n\n        Returns:\n            Ingestion result with document ID\n        \"\"\"\n        payload = {\n            \"content\": content,\n            \"metadata\": metadata.model_dump(),\n            \"document_id\": document_id,\n            \"namespace\": namespace or self.default_namespace,\n        }\n\n        return self._request(\"POST\", \"\u002Fv1\u002Fdocuments\", json=payload)\n\n    def ingest_batch(\n        self,\n        documents: list[tuple[str, DocumentMetadata]],\n        namespace: Optional[str] = None,\n        batch_size: int = 10,\n    ) -> dict:\n        \"\"\"\n        Ingest multiple documents in batches.\n\n        Args:\n            documents: List of (content, metadata) tuples\n            namespace: Optional namespace\n            batch_size: Number of documents per batch\n\n        Returns:\n            Batch ingestion result with success\u002Ffailure counts\n        \"\"\"\n        results = {\"successful\": 0, \"failed\": 0, \"documents\": []}\n        namespace = namespace or self.default_namespace\n\n        for i in range(0, len(documents), batch_size):\n            batch = documents[i:i + batch_size]\n            batch_payload = [\n                {\n                    \"content\": content,\n                    \"metadata\": metadata.model_dump(),\n                    \"namespace\": namespace,\n                }\n                for content, metadata in batch\n            ]\n\n            try:\n                response = self._request(\n                    \"POST\",\n                    \"\u002Fv1\u002Fdocuments\u002Fbatch\",\n                    json={\"documents\": batch_payload}\n                )\n                results[\"successful\"] += len(batch)\n                results[\"documents\"].extend(response.get(\"documents\", []))\n            except Exception as e:\n                results[\"failed\"] += len(batch)\n                # Log failed batch for retry\n\n        return results\n\n    def ingest_from_file(\n        self,\n        file_path: str,\n        metadata: DocumentMetadata,\n        namespace: Optional[str] = None,\n    ) -> dict:\n        \"\"\"\n        Ingest a document from a file.\n\n        Args:\n            file_path: Path to file (markdown, PDF, or HTML)\n            metadata: Document metadata\n            namespace: Optional namespace\n\n        Returns:\n            Ingestion result\n        \"\"\"\n        # Determine document type from extension\n        ext = os.path.splitext(file_path)[1].lower()\n\n        if ext == \".md\":\n            with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                content = f.read()\n            doc_type = DocumentType.MARKDOWN\n        elif ext == \".pdf\":\n            content = self._extract_pdf_text(file_path)\n            doc_type = DocumentType.PDF\n        elif ext in [\".html\", \".htm\"]:\n            with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                content = f.read()\n            content = self._strip_html(content)\n            doc_type = DocumentType.HTML\n        else:\n            # Default to text\n            with open(file_path, \"r\", encoding=\"utf-8\") as f:\n                content = f.read()\n            doc_type = DocumentType.TEXT\n\n        # Update metadata with source file\n        metadata.source_file = file_path\n\n        return self.ingest_document(content, metadata, namespace)\n\n    def _extract_pdf_text(self, file_path: str) -> str:\n        \"\"\"Extract text from PDF file.\"\"\"\n        try:\n            import PyPDF2\n            with open(file_path, \"rb\") as f:\n                reader = PyPDF2.PdfReader(f)\n                text = \"\\n\".join(page.extract_text() for page in reader.pages)\n            return text\n        except ImportError:\n            raise ImportError(\"PyPDF2 required for PDF ingestion: pip install PyPDF2\")\n\n    def _strip_html(self, html: str) -> str:\n        \"\"\"Strip HTML tags from content.\"\"\"\n        import re\n        clean = re.compile(\"\u003C.*?>\")\n        return re.sub(clean, \"\", html)\n\n    # === RETRIEVAL ===\n\n    def search(\n        self,\n        query: str,\n        namespace: Optional[str] = None,\n        filters: Optional[dict] = None,\n        max_chunks: int = 5,\n        min_similarity: float = 0.7,\n        user_role: Optional[str] = None,\n        school_id: Optional[str] = None,\n    ) -> list[ContextDocument]:\n        \"\"\"\n        Search for relevant context documents.\n\n        Args:\n            query: Search query (semantic search)\n            namespace: Namespace to search in\n            filters: Additional metadata filters\n            max_chunks: Maximum number of chunks to return\n            min_similarity: Minimum similarity score threshold\n            user_role: User's role for access control\n            school_id: User's school ID for multi-tenancy\n\n        Returns:\n            List of relevant document chunks\n        \"\"\"\n        # Build search payload with access control\n        payload = {\n            \"query\": query,\n            \"namespace\": namespace or self.default_namespace,\n            \"max_chunks\": max_chunks,\n            \"min_similarity\": min_similarity,\n            \"filters\": filters or {},\n        }\n\n        # Add role-based filtering\n        if user_role:\n            payload[\"filters\"][\"allowed_roles\"] = [user_role, \"all\"]\n\n        # Add tenant filtering\n        if school_id:\n            payload[\"filters\"][\"school_id\"] = school_id\n\n        response = self._request(\"POST\", \"\u002Fv1\u002Fsearch\", json=payload)\n\n        return [\n            ContextDocument(\n                id=doc.get(\"id\"),\n                content=doc.get(\"content\", \"\"),\n                metadata=DocumentMetadata(**doc.get(\"metadata\", {})),\n                chunk_id=doc.get(\"chunk_id\"),\n                similarity_score=doc.get(\"similarity_score\"),\n            )\n            for doc in response.get(\"documents\", [])\n        ]\n\n    def get_document(\n        self,\n        document_id: str,\n        namespace: Optional[str] = None,\n    ) -> Optional[ContextDocument]:\n        \"\"\"\n        Get a specific document by ID.\n\n        Args:\n            document_id: Document ID\n            namespace: Namespace\n\n        Returns:\n            Document or None if not found\n        \"\"\"\n        try:\n            response = self._request(\n                \"GET\",\n                f\"\u002Fv1\u002Fdocuments\u002F{document_id}\",\n                params={\"namespace\": namespace or self.default_namespace}\n            )\n            return ContextDocument(\n                id=response.get(\"id\"),\n                content=response.get(\"content\", \"\"),\n                metadata=DocumentMetadata(**response.get(\"metadata\", {})),\n            )\n        except Exception:\n            return None\n\n    def delete_document(\n        self,\n        document_id: str,\n        namespace: Optional[str] = None,\n    ) -> bool:\n        \"\"\"\n        Delete a document from Context7.\n\n        Args:\n            document_id: Document ID\n            namespace: Namespace\n\n        Returns:\n            True if deleted successfully\n        \"\"\"\n        try:\n            self._request(\n                \"DELETE\",\n                f\"\u002Fv1\u002Fdocuments\u002F{document_id}\",\n                params={\"namespace\": namespace or self.default_namespace}\n            )\n            return True\n        except Exception:\n            return False\n\n    # === MANAGEMENT ===\n\n    def list_documents(\n        self,\n        namespace: Optional[str] = None,\n        module: Optional[str] = None,\n        limit: int = 100,\n    ) -> list[dict]:\n        \"\"\"\n        List documents in a namespace.\n\n        Args:\n            namespace: Namespace to list\n            module: Filter by module\n            limit: Maximum number of results\n\n        Returns:\n            List of document summaries\n        \"\"\"\n        params = {\n            \"namespace\": namespace or self.default_namespace,\n            \"limit\": limit,\n        }\n        if module:\n            params[\"module\"] = module\n\n        response = self._request(\"GET\", \"\u002Fv1\u002Fdocuments\", params=params)\n        return response.get(\"documents\", [])\n\n    def get_stats(self, namespace: Optional[str] = None) -> dict:\n        \"\"\"Get statistics for a namespace.\"\"\"\n        response = self._request(\n            \"GET\",\n            \"\u002Fv1\u002Fstats\",\n            params={\"namespace\": namespace or self.default_namespace}\n        )\n        return response\n\n\n# Singleton instance\n_context7_client: Optional[Context7Client] = None\n\n\ndef get_context7_client() -> Context7Client:\n    \"\"\"Get or create Context7 client singleton.\"\"\"\n    global _context7_client\n    if _context7_client is None:\n        _context7_client = Context7Client()\n    return _context7_client\n```\n\n## Context Shaping Utilities\n\n```python\n# backend\u002Fapp\u002Fservices\u002Fcontext_shaper.py\nfrom typing import list\nfrom backend.app.services.context7_client import ContextDocument\n\n\nclass ContextShaper:\n    \"\"\"Shape and format retrieved context for AI prompts.\"\"\"\n\n    MAX_TOKENS = 4000  # Reserve space for prompt\n    CHUNK_HEADER = \"### Source: {title}\"\n    FOOTER = \"\\n\\n---\\n*Source: {source}*\"\n\n    def shape_for_prompt(\n        self,\n        documents: list[ContextDocument],\n        query: str,\n        max_chunks: int = 5,\n        include_sources: bool = True,\n    ) -> str:\n        \"\"\"\n        Shape retrieved documents into prompt-safe format.\n\n        Args:\n            documents: Retrieved document chunks\n            query: Original search query\n            max_chunks: Maximum chunks to include\n            include_sources: Include source citations\n\n        Returns:\n            Formatted context string\n        \"\"\"\n        chunks = documents[:max_chunks]\n\n        sections = []\n        for i, doc in enumerate(chunks):\n            header = f\"## Chunk {i + 1}\"\n            if doc.metadata.title:\n                header += f\": {doc.metadata.title}\"\n\n            section = header\n            section += f\"\\n\\n{self._format_content(doc.content)}\"\n\n            if include_sources and doc.metadata.source_file:\n                section += self.FOOTER.format(source=doc.metadata.source_file)\n\n            sections.append(section)\n\n        context = \"\\n\\n\".join(sections)\n\n        # Ensure context fits in token limit\n        context = self._truncate_to_token_limit(context, self.MAX_TOKENS)\n\n        return context\n\n    def _format_content(self, content: str) -> str:\n        \"\"\"Format content for readability.\"\"\"\n        # Normalize whitespace\n        lines = content.split(\"\\n\")\n        lines = [line.strip() for line in lines if line.strip()]\n        return \"\\n\".join(lines)\n\n    def _truncate_to_token_limit(self, text: str, max_tokens: int) -> str:\n        \"\"\"Truncate text to fit within token limit.\"\"\"\n        # Rough estimate: 4 characters per token\n        max_chars = max_tokens * 4\n\n        if len(text) \u003C= max_chars:\n            return text\n\n        # Truncate and add note\n        truncated = text[:max_chars - 50]\n        truncated = truncated.rsplit(\"\\n\", 1)[0]  # Don't cut mid-line\n        truncated += \"\\n\\n*... (context truncated for length)*\"\n\n        return truncated\n\n    def format_for_chat(\n        self,\n        documents: list[ContextDocument],\n        user_role: str,\n    ) -> str:\n        \"\"\"\n        Format context for chat widget display.\n\n        Args:\n            documents: Retrieved documents\n            user_role: User's role for messaging\n\n        Returns:\n            User-friendly formatted context\n        \"\"\"\n        if not documents:\n            return \"No relevant information found.\"\n\n        formatted = []\n        for doc in documents:\n            if doc.metadata.title:\n                formatted.append(f\"**{doc.metadata.title}**\")\n            formatted.append(doc.content[:500])  # Limit per chunk\n            formatted.append(\"\")\n\n        return \"\\n\".join(formatted)\n\n\n# Singleton\ncontext_shaper = ContextShaper()\n```\n\n## API Routes\n\n```python\n# backend\u002Fapp\u002Fapi\u002Fknowledge\u002Froutes.py\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom typing import Optional\nfrom pydantic import BaseModel\n\nfrom app.services.context7_client import (\n    get_context7_client,\n    DocumentMetadata,\n    Context7Client,\n)\nfrom app.services.context_shaper import get_context_shaper, ContextShaper\nfrom app.auth.jwt import get_current_user\n\n\nrouter = APIRouter(prefix=\"\u002Fknowledge\", tags=[\"knowledge\"])\n\n\nclass IngestRequest(BaseModel):\n    \"\"\"Request to ingest a document.\"\"\"\n    content: str\n    title: str\n    description: Optional[str] = None\n    module: str\n    category: Optional[str] = None\n    allowed_roles: list[str] = []\n    school_id: Optional[str] = None\n\n\nclass SearchRequest(BaseModel):\n    \"\"\"Request to search knowledge base.\"\"\"\n    query: str\n    module: Optional[str] = None\n    max_chunks: int = 5\n    min_similarity: float = 0.7\n\n\nclass SearchResponse(BaseModel):\n    \"\"\"Search response with shaped context.\"\"\"\n    documents: list[dict]\n    context: str  # Shaped for prompt\n\n\n@router.post(\"\u002Fingest\")\nasync def ingest_document(\n    request: IngestRequest,\n    current_user = Depends(get_current_user),\n    client: Context7Client = Depends(get_context7_client),\n) -> dict:\n    \"\"\"\n    Ingest a document into the knowledge base.\n\n    Requires admin or content-manager role.\n    \"\"\"\n    # Check permissions\n    if \"admin\" not in current_user.roles and \"content-manager\" not in current_user.roles:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail=\"Insufficient permissions to ingest documents\",\n        )\n\n    # Build metadata\n    metadata = DocumentMetadata(\n        title=request.title,\n        description=request.description,\n        allowed_roles=request.allowed_roles or [\"all\"],\n        school_id=request.school_id or current_user.school_id,\n        module=request.module,\n        category=request.category,\n    )\n\n    # Ingest\n    result = client.ingest_document(\n        content=request.content,\n        metadata=metadata,\n    )\n\n    return {\"status\": \"ingested\", \"document_id\": result.get(\"id\")}\n\n\n@router.post(\"\u002Fsearch\", response_model=SearchResponse)\nasync def search_knowledge(\n    request: SearchRequest,\n    current_user = Depends(get_current_user),\n    client: Context7Client = Depends(get_context7_client),\n    shaper: ContextShaper = Depends(get_context_shaper),\n) -> SearchResponse:\n    \"\"\"\n    Search the knowledge base.\n\n    Returns shaped context suitable for AI prompts.\n    \"\"\"\n    # Search with role and tenant filtering\n    documents = client.search(\n        query=request.query,\n        filters={\"module\": request.module} if request.module else {},\n        max_chunks=request.max_chunks,\n        min_similarity=request.min_similarity,\n        user_role=current_user.role,\n        school_id=current_user.school_id,\n    )\n\n    # Shape for prompt\n    context = shaper.shape_for_prompt(\n        documents=documents,\n        query=request.query,\n        max_chunks=request.max_chunks,\n    )\n\n    return SearchResponse(\n        documents=[\n            {\n                \"id\": doc.id,\n                \"title\": doc.metadata.title,\n                \"content\": doc.content[:200],\n                \"similarity\": doc.similarity_score,\n            }\n            for doc in documents\n        ],\n        context=context,\n    )\n\n\n@router.get(\"\u002Fmodules\")\nasync def list_modules(\n    current_user = Depends(get_current_user),\n    client: Context7Client = Depends(get_context7_client),\n) -> dict:\n    \"\"\"List available knowledge modules.\"\"\"\n    documents = client.list_documents(\n        limit=1000,\n    )\n\n    modules = set()\n    for doc in documents:\n        if doc.get(\"metadata\", {}).get(\"school_id\") in [None, current_user.school_id]:\n            modules.add(doc.get(\"metadata\", {}).get(\"module\"))\n\n    return {\"modules\": sorted(modules)}\n```\n\n## Frontend Hook\n\n```typescript\n\u002F\u002F frontend\u002Fhooks\u002FuseContext7Search.ts\nimport { useState, useCallback } from \"react\";\n\ninterface SearchResult {\n  id: string;\n  title: string;\n  content: string;\n  similarity: number;\n}\n\ninterface SearchOptions {\n  module?: string;\n  maxChunks?: number;\n}\n\nexport function useContext7Search() {\n  const [results, setResults] = useState\u003CSearchResult[]>([]);\n  const [context, setContext] = useState\u003Cstring>(\"\");\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState\u003Cstring | null>(null);\n\n  const search = useCallback(async (\n    query: string,\n    options: SearchOptions = {}\n  ) => {\n    setLoading(true);\n    setError(null);\n\n    try {\n      const response = await fetch(\"\u002Fapi\u002Fv1\u002Fknowledge\u002Fsearch\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application\u002Fjson\",\n        },\n        body: JSON.stringify({\n          query,\n          module: options.module,\n          max_chunks: options.maxChunks || 5,\n        }),\n      });\n\n      if (!response.ok) {\n        throw new Error(\"Search failed\");\n      }\n\n      const data = await response.json();\n\n      setResults(data.documents);\n      setContext(data.context);\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Search failed\");\n      setResults([]);\n      setContext(\"\");\n    } finally {\n      setLoading(false);\n    }\n  }, []);\n\n  const clear = useCallback(() => {\n    setResults([]);\n    setContext(\"\");\n    setError(null);\n  }, []);\n\n  return {\n    search,\n    clear,\n    results,\n    context,\n    loading,\n    error,\n  };\n}\n```\n\n## Document Organization\n\n```\ndocs\u002F\n├── policies\u002F\n│   ├── attendance-policy.md\n│   ├── fee-refund-policy.md\n│   └── grading-policy.md\n├── procedures\u002F\n│   ├── student-registration.md\n│   ├── fee-payment.md\n│   └── transcript-request.md\n├── faq\u002F\n│   ├── fees-faq.md\n│   ├── attendance-faq.md\n│   └── grades-faq.md\n└── handbooks\u002F\n    ├── student-handbook.md\n    └── parent-handbook.md\n```\n\n### Metadata Examples\n\n```python\n# Fee policy document\nDocumentMetadata(\n    title=\"Fee Refund Policy\",\n    description=\"Guidelines for fee refunds and cancellations\",\n    module=\"fees\",\n    category=\"policy\",\n    allowed_roles=[\"admin\", \"accountant\", \"parent\", \"student\"],\n    school_id=\"school_001\",\n    language=\"en\",\n    source_file=\"docs\u002Fpolicies\u002Ffee-refund-policy.md\",\n)\n\n# Student FAQ\nDocumentMetadata(\n    title=\"Fee Payment FAQ\",\n    description=\"Common questions about fee payment\",\n    module=\"fees\",\n    category=\"faq\",\n    allowed_roles=[\"student\", \"parent\"],\n    school_id=\"school_001\",\n    language=\"en\",\n    source_file=\"docs\u002Ffaq\u002Ffees-faq.md\",\n)\n\n# Staff procedure\nDocumentMetadata(\n    title=\"Student Registration Procedure\",\n    description=\"Step-by-step guide for registering new students\",\n    module=\"registration\",\n    category=\"procedure\",\n    allowed_roles=[\"admin\", \"registrar\"],\n    school_id=\"school_001\",\n    language=\"en\",\n    source_file=\"docs\u002Fprocedures\u002Fstudent-registration.md\",\n)\n```\n\n## Quality Checklist\n\n- [ ] **No PII ingestion**: Sensitive documents reviewed before upload\n- [ ] **Query bounds**: max_chunks, min_similarity prevent excessive results\n- [ ] **Error handling**: Graceful fallback when context unavailable\n- [ ] **Source attribution**: Clear indication when answers are document-based\n- [ ] **Multi-tenancy**: school_id filtering prevents cross-tenant access\n- [ ] **Role filtering**: allowed_roles field controls access\n- [ ] **Token limits**: Context truncation prevents prompt overflow\n\n## Integration Points\n\n| Skill | Integration |\n|-------|-------------|\n| `@jwt-auth` | Extract role and school_id from JWT for access control |\n| `@api-client` | API calls for ingest\u002Fsearch endpoints |\n| `@chatkit-widget` | Provide context for AI-powered chat |\n| `@fastapi-app` | Register knowledge API routes |\n| `@error-handling` | Handle context retrieval errors gracefully |\n\n## Multi-Tenancy\n\n```python\n# Per-school namespace isolation\nclass Context7Client:\n    # ...\n\n    def search(self, query: str, school_id: str, **kwargs) -> list[ContextDocument]:\n        # Always filter by school_id\n        return super().search(\n            query,\n            school_id=school_id,\n            filters={\"school_id\": school_id},\n            **kwargs\n        )\n\n    def ingest_document(\n        self,\n        content: str,\n        metadata: DocumentMetadata,\n        school_id: str,\n        **kwargs\n    ) -> dict:\n        # Always set school_id on metadata\n        metadata.school_id = school_id\n        return super().ingest_document(content, metadata, **kwargs)\n```\n\n## Batch Ingestion Script\n\n```python\n# scripts\u002Fingest_docs.py\n#!\u002Fusr\u002Fbin\u002Fenv python3\n\"\"\"Batch ingest documentation into Context7.\"\"\"\nimport os\nimport sys\nfrom pathlib import Path\n\n# Add project to path\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom app.services.context7_client import Context7Client, DocumentMetadata\n\n\ndef ingest_directory(\n    dir_path: str,\n    module: str,\n    category: str,\n    school_id: str,\n    allowed_roles: list[str],\n):\n    \"\"\"Ingest all documents in a directory.\"\"\"\n    client = Context7Client()\n    dir_path = Path(dir_path)\n\n    for file_path in dir_path.rglob(\"*.md\"):\n        print(f\"Ingesting: {file_path}\")\n\n        metadata = DocumentMetadata(\n            title=file_path.stem.replace(\"-\", \" \").title(),\n            module=module,\n            category=category,\n            school_id=school_id,\n            allowed_roles=allowed_roles,\n            source_file=str(file_path),\n        )\n\n        try:\n            client.ingest_from_file(str(file_path), metadata)\n            print(f\"  ✓ Ingested\")\n        except Exception as e:\n            print(f\"  ✗ Failed: {e}\")\n\n\nif __name__ == \"__main__\":\n    import argparse\n\n    parser = argparse.ArgumentParser(description=\"Batch ingest documents\")\n    parser.add_argument(\"--dir\", required=True, help=\"Directory to ingest\")\n    parser.add_argument(\"--module\", required=True, help=\"Document module\")\n    parser.add_argument(\"--category\", required=True, help=\"Document category\")\n    parser.add_argument(\"--school-id\", required=True, help=\"School ID\")\n    parser.add_argument(\"--roles\", default=\"all\", help=\"Comma-separated allowed roles\")\n\n    args = parser.parse_args()\n\n    ingest_directory(\n        args.dir,\n        args.module,\n        args.category,\n        args.school_id,\n        args.roles.split(\",\"),\n    )\n```\n\n## Error Handling\n\n```python\n# backend\u002Fapp\u002Fservices\u002Fcontext7_client.py\n\nclass Context7Error(Exception):\n    \"\"\"Base exception for Context7 errors.\"\"\"\n    pass\n\n\nclass Context7SearchError(Context7Error):\n    \"\"\"Error during context search.\"\"\"\n    pass\n\n\nclass Context7IngestError(Context7Error):\n    \"\"\"Error during document ingestion.\"\"\"\n    pass\n\n\n# Usage in search\ndef search(self, *args, **kwargs) -> list[ContextDocument]:\n    try:\n        return self._search_impl(*args, **kwargs)\n    except Exception as e:\n        raise Context7SearchError(f\"Search failed: {e}\") from e\n\n\n# Frontend fallback\nfunction useContext7Search() {\n  const { search, results, loading, error } = useContext7Search();\n\n  \u002F\u002F Fallback to general response if context fails\n  const handleSearch = async (query: string) => {\n    try {\n      await search(query);\n    } catch {\n      \u002F\u002F Use generic response\n      setResults([]);\n      setContext(\"\");\n    }\n  };\n\n  return { search: handleSearch, results, loading, error };\n}\n```\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fcontext7-integration",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill context7-integration",{"tool":64,"command":65},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill context7-integration",{"tool":67,"command":68},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill context7-integration","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fcontext7-integration\u002FSKILL.md",true,[72,78],{"path":73,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":76,"sourceHtmlUrl":77},"scripts\u002Fverify.py","SCRIPT","脚本",1902,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fcontext7-integration\u002Fscripts\u002Fverify.py",{"path":79,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":82,"sourceHtmlUrl":83},"skill-report.json","REFERENCE","参考文档",10488,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fawais68\u002Fcontext7-integration\u002Fskill-report.json",[85,97,107,116,122,129],{"skillNo":86,"name":87,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":93,"publishedAt":44},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[90],{"categoryNo":91,"categoryName":92,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[94,95,96],"model","model hierarchy","model hierarchy skill",{"skillNo":98,"name":99,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":100,"categories":101,"keywords":103,"publishedAt":44},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[102],{"categoryNo":91,"categoryName":92,"taxonomyType":21,"taxonomyTypeDesc":22},[104,105,106],"MCP","zoom","zoom mcp",{"skillNo":108,"name":109,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":110,"categories":111,"keywords":112,"publishedAt":44},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[113,114,115,105],"sdk","zoom apps sdk","apps",{"skillNo":117,"name":118,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":119,"categories":120,"keywords":121,"publishedAt":44},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[118],{"skillNo":123,"name":124,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":125,"categories":126,"keywords":128,"publishedAt":44},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[127],{"categoryNo":91,"categoryName":92,"taxonomyType":21,"taxonomyTypeDesc":22},[124],{"skillNo":130,"name":131,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":132,"categories":133,"keywords":134,"publishedAt":44},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[131]]