Generated banner for Pinecone Assistant
Generated logo for Pinecone Assistant

Pinecone Assistant

PineconeOfficialPublished by Pinecone

  • Skill
  • Tool integration
  • Data and AI
  • MIT
  • Low risk
  • Has scripts

From the author

Create, manage, and chat with Pinecone Assistants for document Q&A with citations. Handles all assistant operations - create, upload, sync, chat, context retrieval, and list. Recognizes natural language like "create an assistant from my docs", "ask my assistant about X", or "upload my docs to Pinecone".
— the description by Pinecone

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Pinecone Assistant is a managed RAG service that enables users to upload documents and perform natural language queries with citations. This skill provides a set of scripts to automate the lifecycle of these assistants, including creation, file management, document synchronization, conversational Q&A, and raw context retrieval. It simplifies the integration by handling the underlying infrastructure, allowing users to focus on querying their data.

What it does

  • Create and configure new Pinecone Assistants.
  • Upload and index documents for knowledge base population.
  • Synchronize local files with the assistant's knowledge base.
  • Perform conversational Q&A with cited responses.
  • Retrieve raw context snippets for debugging or custom workflows.
  • List and manage existing assistants.

When to use it

  • When you need to build a RAG-enabled assistant without managing embedding pipelines.
  • When you want to query specific documentation or local files using natural language.
  • When you need to verify the relevance of indexed content through raw context retrieval.
  • When you want to automate the synchronization of local document folders with a cloud assistant.

Advantages

  • Fully managed infrastructure eliminates the need for manual embedding pipelines.
  • Provides cited answers for improved transparency and verification.
  • Supports incremental synchronization to keep knowledge bases up to date.
  • Enables both conversational chat and raw data retrieval for flexible use cases.

Usage examples

“Create an assistant called docs-bot and upload my ./docs folder.”
The agent initializes a new Pinecone Assistant named docs-bot and indexes all files found in the specified local directory.
“Ask my assistant about the main features of the project.”
The agent sends the query to the active assistant and returns a conversational response with relevant citations.
“Find context about authentication in my assistant.”
The agent retrieves and displays raw text snippets related to authentication from the assistant's knowledge base.
“Sync my docs with the assistant.”
The agent performs an incremental update, ensuring the assistant's knowledge base reflects the current state of the local files.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/pinecone-io/pinecone-assistant/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip pinecone-assistant.zip -d .agents/skills

Documentation by Pinecone

Pinecone Assistant

Pinecone Assistant is a fully managed RAG service. Upload documents, ask questions, get cited answers. No embedding pipelines or infrastructure required.

All scripts are in scripts/ relative to this skill directory. Run with: uv run scripts/script_name.py [arguments]

<<clarify_style>>

Operations

What to doScriptKey args
Create an assistantscripts/create.py--name --instructions --region
Upload filesscripts/upload.py--assistant --source --patterns
Sync files (incremental)scripts/sync.py--assistant --source --delete-missing --dry-run
Chat / ask a questionscripts/chat.py--assistant --message
Get context snippetsscripts/context.py--assistant --query --top-k
List assistantsscripts/list.py--files --json

For full workflow details on any operation, read the relevant file in references/.


Natural Language Recognition

Proactively handle these patterns without requiring explicit commands:

Create: "create an assistant", "make an assistant called X", "set up an assistant for my docs" → See references/create.md

Upload: "upload my docs", "add files to my assistant", "index my documentation" → See references/upload.md

Sync: "sync my docs", "update my assistant", "keep assistant in sync", "refresh from ./docs" → See references/sync.md

Chat: "ask my assistant about X", "what does my assistant know about X", "chat with X" → See references/chat.md

Context: "search my assistant for X", "find context about X" → See references/context.md

List: "show my assistants", "what assistants do I have" → Run uv run scripts/list.py


Conversation Memory

Track the last assistant used within the conversation:

  • When a user creates or first uses an assistant, remember its name

  • If user says "my assistant", "it", or "the assistant" → use the last one

  • Briefly confirm which assistant you're using: "Asking docs-bot..."

  • If ambiguous and multiple exist → ask the user to clarify


Multi-Step Requests

Handle chained requests naturally. Example:

"Create an assistant called docs-bot, upload my ./docs folder, and ask what the main features are"

  1. uv run scripts/create.py --name docs-bot

  2. uv run scripts/upload.py --assistant docs-bot --source ./docs

  3. uv run scripts/chat.py --assistant docs-bot --message "what are the main features?"


Prerequisites

Files

scripts/sync.py

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#   "pinecone==9.1.0",
#   "typer>=0.15.0",
#   "rich>=13.0.0",
# ]
# ///
"""
Sync local files to a Pinecone Assistant, only uploading new or changed files.

Usage:
    uv run sync.py --assistant NAME --source PATH [--delete-missing] [--dry-run]

Environment Variables:
    PINECONE_API_KEY: Required Pinecone API key

Output:
    Shows files to add, update, and optionally delete, with confirmation prompt
"""

import os
import hashlib
from pathlib import Path
from datetime import datetime, timezone
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, TextColumn
from pinecone import Pinecone

app = typer.Typer()
console = Console()

# Supported file extensions
SUPPORTED_EXTENSIONS = {'.md', '.txt', '.pdf', '.docx', '.json'}

# Directories to exclude
EXCLUDE_DIRS = {'node_modules', '.venv', '.git', 'build', 'dist', '__pycache__', '.pytest_cache'}


def should_exclude_path(path: Path, source_root: Path) -> bool:
    """Check if path should be excluded based on directory patterns."""
    try:
        rel_path = path.relative_to(source_root)
        for part in rel_path.parts:
            if part in EXCLUDE_DIRS or part.startswith('.'):
                return True
    except ValueError:
        return True
    return False


def find_files(source_path: Path) -> list[Path]:
    """Find all supported files in source directory, excluding common build/dependency dirs."""
    files = []

    if source_path.is_file():
        if source_path.suffix.lower() in SUPPORTED_EXTENSIONS:
            return [source_path]
        else:
            return []

    for file_path in source_path.rglob('*'):
        if file_path.is_file():
            if file_path.suffix.lower() in SUPPORTED_EXTENSIONS:
                if not should_exclude_path(file_path, source_path):
                    files.append(file_path)

    return sorted(files)


def get_file_info(file_path: Path):
    """Get file modification time and size."""
    stat = file_path.stat()
    return {
        'mtime': stat.st_mtime,
        'size': stat.st_size,
    }


def file_changed(local_info: dict, remote_metadata: dict) -> bool:
    """Check if local file differs from remote using mtime and size."""
    remote_mtime = remote_metadata.get('mtime')
    remote_size = remote_metadata.get('size')

    if remote_mtime is None or remote_size is None:
        # No stored metadata, assume changed
        return True

    return (local_info['mtime'] != float(remote_mtime) or
            local_info['size'] != int(remote_size))


@app.command()
def main(
    assistant: str = typer.Option(..., "--assistant", "-a", help="Name of the assistant"),
    source: str = typer.Option(..., "--source", "-s", help="Local file or directory path"),
    delete_missing: bool = typer.Option(False, "--delete-missing", help="Delete files from assistant that don't exist locally"),
    dry_run: bool = typer.Option(False, "--dry-run", help="Show what would change without making changes"),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
):
    """Sync local files to Pinecone Assistant, only uploading new or changed files."""

    # Check for API key
    api_key = os.environ.get("PINECONE_API_KEY")
    if not api_key:
        console.print("[red]Error: PINECONE_API_KEY environment variable not set[/red]")
        console.print("\nGet your API key from: https://app.pinecone.io/?sessionType=signup")
        raise typer.Exit(1)

    # Validate source path
    source_path = Path(source).resolve()
    if not source_path.exists():
        console.print(f"[red]Error: Source path does not exist: {source}[/red]")
        raise typer.Exit(1)

    try:
        # Initialize Pinecone client
        pc = Pinecone(api_key=api_key, source_tag="pinecone_skills:assistant")
        # describe() returns a model carrying a client back-reference, so the
        # asst.list_files()/upload_file()/delete_file() calls below keep working.
        # Its list_files() also materializes, unlike pc.assistants.list_files(),
        # which returns a Paginator with no __len__.
        asst = pc.assistants.describe(name=assistant)

        console.print(Panel(
            f"[bold cyan]Assistant:[/bold cyan] {assistant}\n"
            f"[bold cyan]Source:[/bold cyan] {source_path}",
            title="Sync Configuration",
            border_style="cyan"
        ))

        # Step 1: Get current files in assistant
        with console.status("[bold blue]Fetching assistant files...[/bold blue]", spinner="dots"):
            remote_files = asst.list_files()

        # Build map of file_path -> file object
        remote_file_map = {}
        for f in remote_files:
            metadata = getattr(f, 'metadata', {}) or {}
            file_path = metadata.get('file_path', f.name)
            remote_file_map[file_path] = {
                'file_obj': f,
                'metadata': metadata
            }

        console.print(f"[dim]Found {len(remote_files)} file(s) in assistant[/dim]\n")

        # Step 2: Find local files
        with console.status("[bold blue]Scanning local files...[/bold blue]", spinner="dots"):
            local_files = find_files(source_path)

        if not local_files:
            console.print("[yellow]No supported files found in source path[/yellow]")
            console.print(f"Supported extensions: {', '.join(sorted(SUPPORTED_EXTENSIONS))}")
            raise typer.Exit(0)

        console.print(f"[dim]Found {len(local_files)} local file(s)[/dim]\n")

        # Step 3: Determine what needs syncing
        to_upload = []  # New files
        to_update = []  # Changed files (delete + re-upload)
        to_delete = []  # Files in assistant but not local
        unchanged = []  # Files that match

        # Track which remote files we've seen
        seen_remote_paths = set()

        for local_file in local_files:
            # Get relative path from source root
            if source_path.is_file():
                rel_path = local_file.name
            else:
                rel_path = str(local_file.relative_to(source_path))

            local_info = get_file_info(local_file)

            if rel_path in remote_file_map:
                # File exists remotely, check if changed
                seen_remote_paths.add(rel_path)
                remote_info = remote_file_map[rel_path]

                if file_changed(local_info, remote_info['metadata']):
                    to_update.append({
                        'local_path': local_file,
                        'rel_path': rel_path,
                        'remote_file_id': remote_info['file_obj'].id,
                        'local_info': local_info
                    })
                else:
                    unchanged.append(rel_path)
            else:
                # New file
                to_upload.append({
                    'local_path': local_file,
                    'rel_path': rel_path,
                    'local_info': local_info
                })

        # Find files to delete (in remote but not local)
        if delete_missing:
            for rel_path, remote_info in remote_file_map.items():
                if rel_path not in seen_remote_paths:
                    to_delete.append({
                        'rel_path': rel_path,
                        'remote_file_id': remote_info['file_obj'].id
                    })

        # Step 4: Show summary
        console.print("[bold]Sync Summary:[/bold]\n")

        summary_table = Table(show_header=True, header_style="bold cyan")
        summary_table.add_column("Action", style="yellow", width=15)
        summary_table.add_column("Count", style="green", width=10)

        summary_table.add_row("New files", str(len(to_upload)))
        summary_table.add_row("Updated files", str(len(to_update)))
        if delete_missing:
            summary_table.add_row("Deleted files", str(len(to_delete)))
        summary_table.add_row("Unchanged", str(len(unchanged)))

        console.print(summary_table)
        console.print()

        # Show details if there are changes
        if to_upload:
            console.print("[bold green]Files to upload:[/bold green]")
            for item in to_upload[:10]:  # Show first 10
                console.print(f"  + {item['rel_path']}")
            if len(to_upload) > 10:
                console.print(f"  ... and {len(to_upload) - 10} more")
            console.print()

        if to_update:
            console.print("[bold yellow]Files to update:[/bold yellow]")
            for item in to_update[:10]:
                console.print(f"  ~ {item['rel_path']}")
            if len(to_update) > 10:
                console.print(f"  ... and {len(to_update) - 10} more")
            console.print()

        if to_delete:
            console.print("[bold red]Files to delete:[/bold red]")
            for item in to_delete[:10]:
                console.print(f"  - {item['rel_path']}")
            if len(to_delete) > 10:
                console.print(f"  ... and {len(to_delete) - 10} more")
            console.print()

        # If no changes, exit early
        if not (to_upload or to_update or to_delete):
            console.print("[green]✓ All files are up to date![/green]")
            return

        # Dry run mode
        if dry_run:
            console.print("[yellow]Dry run mode: No changes made[/yellow]")
            return

        # Confirmation prompt
        if not yes:
            proceed = typer.confirm("\nProceed with sync?")
            if not proceed:
                console.print("[yellow]Sync cancelled[/yellow]")
                return

        console.print()

        # Step 5: Execute sync
        uploaded_count = 0
        updated_count = 0
        deleted_count = 0

        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            console=console
        ) as progress:

            # Upload new files
            if to_upload:
                task = progress.add_task(f"Uploading {len(to_upload)} new file(s)...", total=len(to_upload))
                for item in to_upload:
                    try:
                        asst.upload_file(
                            file_path=str(item['local_path']),
                            metadata={
                                'file_path': item['rel_path'],
                                'mtime': item['local_info']['mtime'],
                                'size': item['local_info']['size'],
                                'uploaded_at': datetime.now(timezone.utc).isoformat(),
                                'source': 'sync_script',
                            },
                            timeout=None
                        )
                        uploaded_count += 1
                        progress.advance(task)
                    except Exception as e:
                        console.print(f"[red]Failed to upload {item['rel_path']}: {e}[/red]")

            # Update changed files (delete old + upload new)
            if to_update:
                task = progress.add_task(f"Updating {len(to_update)} file(s)...", total=len(to_update) * 2)
                for item in to_update:
                    try:
                        # Delete old version
                        asst.delete_file(file_id=item['remote_file_id'])
                        progress.advance(task)

                        # Upload new version
                        asst.upload_file(
                            file_path=str(item['local_path']),
                            metadata={
                                'file_path': item['rel_path'],
                                'mtime': item['local_info']['mtime'],
                                'size': item['local_info']['size'],
                                'uploaded_at': datetime.now(timezone.utc).isoformat(),
                                'source': 'sync_script',
                            },
                            timeout=None
                        )
                        updated_count += 1
                        progress.advance(task)
                    except Exception as e:
                        console.print(f"[red]Failed to update {item['rel_path']}: {e}[/red]")

            # Delete missing files
            if to_delete:
                task = progress.add_task(f"Deleting {len(to_delete)} file(s)...", total=len(to_delete))
                for item in to_delete:
                    try:
                        asst.delete_file(file_id=item['remote_file_id'])
                        deleted_count += 1
                        progress.advance(task)
                    except Exception as e:
                        console.print(f"[red]Failed to delete {item['rel_path']}: {e}[/red]")

        # Final summary
        console.print()
        console.print(Panel(
            f"[green]✓ Sync complete![/green]\n\n"
            f"Uploaded: {uploaded_count}\n"
            f"Updated: {updated_count}\n"
            + (f"Deleted: {deleted_count}\n" if delete_missing else "") +
            f"Unchanged: {len(unchanged)}",
            title="Results",
            border_style="green"
        ))

    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1)


if __name__ == "__main__":
    app()

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an API key, Needs Python

Security

What our scanner noticed· Low risk

  • Instructions that use the network
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Pinecone
Publisher
Pinecone (Official)
Source
pinecone-io/skills · skills/pinecone-assistant · commit 885dc53
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Pinecone or Pinecone.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download pinecone-assistant.zip