Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

## [0.6.0]
### Changed
- Updated CLI chat command to `vs chat`
- Refactored CLI into separate modules

## [0.5.1]
### Fixed
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Duration: 8.93 seconds

Sync from a Zotero collection. Interactive selections are remembered for future sessions.
```bash
vs sync -s zotero
vs sync -source zotero

Enter the path to your Zotero directory (Default: /Users/jbencina/Zotero):

Expand All @@ -96,11 +96,11 @@ vs settings clear
```

#### Chat Interactions
Use `vs assistant chat` to chat with uploaded documents via the command line. The responding assistant is automatically linked to your
vector store. Alternatively, you can use `vs assistant ui` to spawn a local Gradio instance.
Use `vs chat` to chat with uploaded documents via the command line. The responding assistant is automatically linked to your
vector store. Alternatively, you can use `vs chat -u` to spawn a local Gradio instance.

```bash
vs assistant chat
vs chat
✅ Assistant found: asst_123456789
Type "exit" to quit at any time.

Expand All @@ -112,7 +112,7 @@ The contents of the vector store collection primarily focus on machine learning

Conversations are remembered across sessions.
```bash
vs assistant chat
vs chat
✅ Assistant found: asst_123456789
✅ Thread found: thread_123456789
Type "exit" to quit at any time.
Expand All @@ -123,7 +123,7 @@ Your last question to me was asking for a one sentence summary of the contents o

Threads can be cleared using the `-n` flag.
```bash
vs assistant chat -n
vs chat -n
✅ Assistant found: asst_123456789
Type "exit" to quit at any time.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ dependencies = [
]

[project.scripts]
vs = "vecsync.cli:cli"
vs = "vecsync.cli.entry:cli"

[project.urls]
Homepage = "https://vecsync.io"
Expand Down
Empty file added src/vecsync/cli/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions src/vecsync/cli/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import click

from vecsync.chat.clients.openai import OpenAIClient
from vecsync.chat.interface import ConsoleInterface, GradioInterface
from vecsync.constants import DEFAULT_STORE_NAME


def start_console_chat(store_name: str, new_conversation: bool):
client = OpenAIClient(store_name=store_name, new_conversation=new_conversation)
ui = ConsoleInterface(client)
print('Type "exit" to quit at any time.')

while True:
print()
prompt = input("> ")
if prompt.lower() == "exit":
break
ui.prompt(prompt)


def start_ui_chat(store_name: str, new_conversation: bool):
client = OpenAIClient(store_name=store_name, new_conversation=new_conversation)
ui = GradioInterface(client)
ui.chat_interface()


@click.command("chat")
@click.option(
"--new-conversation",
"-n",
is_flag=True,
help="Force the assistant to create a new thread.",
)
@click.option(
"--use-ui",
"-u",
is_flag=True,
help="Spawn an interactive UI instead of a console interface.",
)
def chat(new_conversation: bool, use_ui: bool):
"""Chat with the assistant."""

if use_ui:
start_ui_chat(DEFAULT_STORE_NAME, new_conversation)
else:
start_console_chat(DEFAULT_STORE_NAME, new_conversation)
19 changes: 19 additions & 0 deletions src/vecsync/cli/entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import click

from vecsync.cli.chat import chat
from vecsync.cli.settings import group as settings_group
from vecsync.cli.store import group as store_group
from vecsync.cli.sync import sync


@click.group()
def cli():
"""vecsync CLI tool"""
pass


for group in [store_group, settings_group]:
cli.add_command(group)

cli.add_command(sync)
cli.add_command(chat)
30 changes: 30 additions & 0 deletions src/vecsync/cli/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import click
from termcolor import colored

from vecsync.settings import Settings


@click.command()
def clear():
"""Clear the settings file."""
settings = Settings()
settings.delete()


@click.command()
def info():
"""Get the location and data of the settings file."""
settings = Settings()
data = settings.info()
click.echo(f"Settings file location: {colored(data.location, 'yellow')}")
click.echo(f"Settings file data:\n{colored(data.data, 'yellow')}")


@click.group(name="settings")
def group():
"""Commands to manage application settings"""
pass


group.add_command(clear)
group.add_command(info)
35 changes: 35 additions & 0 deletions src/vecsync/cli/store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import click
from termcolor import cprint

from vecsync.constants import DEFAULT_STORE_NAME
from vecsync.store.openai import OpenAiVectorStore


@click.command()
def list():
"""List files in the remote vector store."""
store = OpenAiVectorStore(DEFAULT_STORE_NAME)
files = store.get_files()

num_total = len(files)

cprint(f"{num_total} Files in store '{store.name}':", "green")
for file in files:
cprint(f"\t✅{file.name}", "yellow")


@click.command()
def delete():
"""Delete all files in the remote vector store."""
vstore = OpenAiVectorStore(DEFAULT_STORE_NAME)
vstore.delete()


@click.group(name="store")
def group():
"""Commands to manage the vector store."""
pass


group.add_command(list)
group.add_command(delete)
44 changes: 44 additions & 0 deletions src/vecsync/cli/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import click
from termcolor import cprint

from vecsync.constants import DEFAULT_STORE_NAME
from vecsync.store.file import FileStore
from vecsync.store.openai import OpenAiVectorStore
from vecsync.store.zotero import ZoteroStore


@click.command()
@click.option(
"--source",
type=str,
default="file",
help="Choose the source (file or zotero).",
)
def sync(source: str):
"""Sync files from local to remote vector store."""
if source == "file":
store = FileStore()
elif source == "zotero":
try:
store = ZoteroStore.client()
except FileNotFoundError as e:
cprint(f'Zotero not found at "{str(e)}". Aborting.', "red")
return
else:
raise ValueError("Invalid source. Use 'file' or 'zotero'.")

vstore = OpenAiVectorStore(DEFAULT_STORE_NAME)
vstore.get_or_create()

files = store.get_files()

cprint(f"Syncing {len(files)} files from local to OpenAI", "green")

result = vstore.sync(files)
cprint("🏁 Sync results:", "green")
cprint(
f"Saved: {result.files_saved} | Deleted: {result.files_deleted} | Skipped: {result.files_skipped} ",
"yellow",
)
cprint(f"Remote count: {result.updated_count}", "yellow")
cprint(f"Duration: {result.duration:.2f} seconds", "yellow")
1 change: 1 addition & 0 deletions src/vecsync/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT_STORE_NAME = "test"
9 changes: 9 additions & 0 deletions src/vecsync/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class SettingMissing(BaseModel):
key: str


class SettingData(BaseModel):
location: str
data: str


class Settings:
def __init__(self, path: str | None = None):
self.file = path or Path(user_config_dir("vecsync")) / "settings.json"
Expand All @@ -31,6 +36,10 @@ def delete(self):
if self.file.exists():
self.file.unlink()

def info(self) -> SettingData:
"""Get the location and data of the settings file."""
return SettingData(location=str(self.file), data=self.file.read_text())

def __getitem__(self, key: str) -> SettingExists | SettingMissing:
with open(self.file) as f:
data = json.load(f)
Expand Down
8 changes: 8 additions & 0 deletions src/vecsync/store/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# pragma: exclude file

from enum import Enum

from pydantic import BaseModel


class FileStatus(str, Enum):
ATTACHED = "attached"
DETACHED = "detached"


class StoredFile(BaseModel):
id: str
name: str
status: FileStatus
22 changes: 19 additions & 3 deletions src/vecsync/store/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from termcolor import cprint
from tqdm import tqdm

from vecsync.store.base import StoredFile
from vecsync.store.base import FileStatus, StoredFile


class SyncOperationResult(BaseModel):
Expand Down Expand Up @@ -38,8 +38,24 @@ def get(self):
raise ValueError(f"Vector store with name {self.name} not found.")

def get_files(self) -> list[StoredFile]:
files = self.client.files.list()
return [StoredFile(id=file.id, name=file.filename) for file in files]
if not self.store:
self.get()

uploaded_files = self.client.files.list()
vector_store_files = set([f.id for f in self.client.vector_stores.files.list(vector_store_id=self.store.id)])

files = []

for file in uploaded_files:
files.append(
StoredFile(
id=file.id,
name=file.filename,
status=FileStatus.ATTACHED if file.id in vector_store_files else FileStatus.DETACHED,
)
)

return files

def get_or_create(self):
try:
Expand Down
Loading