bookworm_genai 0.1.1b3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.1
2
+ Name: bookworm_genai
3
+ Version: 0.1.1b3
4
+ Summary: Bookworm - A LLM-powered bookmark search engine
5
+ Author: kiran94
6
+ Requires-Python: >=3.9,<4.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: duckdb (>=1.0.0,<2.0.0)
13
+ Requires-Dist: jq (>=1.7.0,<2.0.0)
14
+ Requires-Dist: langchain (>=0.2.12,<0.3.0)
15
+ Requires-Dist: langchain-community (>=0.2.11,<0.3.0)
16
+ Requires-Dist: langchain-openai (>=0.1.20,<0.2.0)
17
+ Requires-Dist: pandas (>=2.2.2,<3.0.0)
18
+ Requires-Dist: platformdirs (>=4.2.2,<5.0.0)
19
+ Requires-Dist: rich (>=13.7.1,<14.0.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # bookworm
23
+
24
+ ### Processes
25
+
26
+ *`bookworm sync`*
27
+
28
+ ```python
29
+ python -m bookworm sync
30
+ ```
31
+
32
+ ```mermaid
33
+ graph LR
34
+
35
+ subgraph Bookmarks
36
+ Chrome(Chrome Bookmarks)
37
+ Brave(Brave Bookmarks)
38
+ end
39
+
40
+ Bookworm(bookworm sync)
41
+
42
+ EmbeddingsService(Embeddings Service e.g OpenAIEmbeddings)
43
+
44
+ VectorStore(Vector Store e.g DuckDB)
45
+
46
+ Chrome -->|load bookmarks|Bookworm
47
+ Brave -->|load bookmarks|Bookworm
48
+
49
+ Bookworm -->|vectorize bookmarks|EmbeddingsService-->|store embeddings|VectorStore
50
+ ```
51
+
52
+ ---
53
+
54
+ *`bookworm ask`*
55
+
56
+ ```python
57
+ python -m bookworm ask
58
+ ```
59
+
60
+ ```mermaid
61
+ graph LR
62
+
63
+ query
64
+ Bookworm(bookworm ask)
65
+
66
+ subgraph _
67
+ LLM(LLM e.g OpenAI)
68
+ VectorStore(Vector Store e.g DuckDB)
69
+ end
70
+
71
+ query -->|user queries for information|Bookworm
72
+
73
+ Bookworm -->|simularity search|VectorStore -->|send similar docs + user query|LLM
74
+ LLM -->|send back response|Bookworm
75
+ ```
76
+
77
+ ---
78
+
79
+
80
+ ### Developer Setup
81
+
82
+ ```bash
83
+ # LLMs
84
+ export OPENAI_API_KEY=
85
+
86
+ # Langchain (optional, but useful for debugging)
87
+ export LANGCHAIN_API_KEY=
88
+ export LANGCHAIN_TRACING_V2=true
89
+ export LANGCHAIN_PROJECT=bookworm
90
+
91
+ # Misc (optional)
92
+ export LOGGING_LEVEL=INFO
93
+ ```
94
+
95
+ Recommendations:
96
+
97
+ - Install [`pyenv`](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) and ensure [build dependencies are installed](https://github.com/pyenv/pyenv?tab=readme-ov-file#install-python-build-dependencies) for your OS.
98
+ - Install [Poetry](https://python-poetry.org/docs/) we will be using [environment management](https://python-poetry.org/docs/managing-environments/) below.
99
+
100
+
101
+ ```bash
102
+ poetry env use 3.9 # or path to your 3.9 installation
103
+
104
+ poetry shell
105
+ poetry install
106
+
107
+ bookworm --help
108
+ ```
109
+
@@ -0,0 +1,87 @@
1
+ # bookworm
2
+
3
+ ### Processes
4
+
5
+ *`bookworm sync`*
6
+
7
+ ```python
8
+ python -m bookworm sync
9
+ ```
10
+
11
+ ```mermaid
12
+ graph LR
13
+
14
+ subgraph Bookmarks
15
+ Chrome(Chrome Bookmarks)
16
+ Brave(Brave Bookmarks)
17
+ end
18
+
19
+ Bookworm(bookworm sync)
20
+
21
+ EmbeddingsService(Embeddings Service e.g OpenAIEmbeddings)
22
+
23
+ VectorStore(Vector Store e.g DuckDB)
24
+
25
+ Chrome -->|load bookmarks|Bookworm
26
+ Brave -->|load bookmarks|Bookworm
27
+
28
+ Bookworm -->|vectorize bookmarks|EmbeddingsService-->|store embeddings|VectorStore
29
+ ```
30
+
31
+ ---
32
+
33
+ *`bookworm ask`*
34
+
35
+ ```python
36
+ python -m bookworm ask
37
+ ```
38
+
39
+ ```mermaid
40
+ graph LR
41
+
42
+ query
43
+ Bookworm(bookworm ask)
44
+
45
+ subgraph _
46
+ LLM(LLM e.g OpenAI)
47
+ VectorStore(Vector Store e.g DuckDB)
48
+ end
49
+
50
+ query -->|user queries for information|Bookworm
51
+
52
+ Bookworm -->|simularity search|VectorStore -->|send similar docs + user query|LLM
53
+ LLM -->|send back response|Bookworm
54
+ ```
55
+
56
+ ---
57
+
58
+
59
+ ### Developer Setup
60
+
61
+ ```bash
62
+ # LLMs
63
+ export OPENAI_API_KEY=
64
+
65
+ # Langchain (optional, but useful for debugging)
66
+ export LANGCHAIN_API_KEY=
67
+ export LANGCHAIN_TRACING_V2=true
68
+ export LANGCHAIN_PROJECT=bookworm
69
+
70
+ # Misc (optional)
71
+ export LOGGING_LEVEL=INFO
72
+ ```
73
+
74
+ Recommendations:
75
+
76
+ - Install [`pyenv`](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) and ensure [build dependencies are installed](https://github.com/pyenv/pyenv?tab=readme-ov-file#install-python-build-dependencies) for your OS.
77
+ - Install [Poetry](https://python-poetry.org/docs/) we will be using [environment management](https://python-poetry.org/docs/managing-environments/) below.
78
+
79
+
80
+ ```bash
81
+ poetry env use 3.9 # or path to your 3.9 installation
82
+
83
+ poetry shell
84
+ poetry install
85
+
86
+ bookworm --help
87
+ ```
@@ -0,0 +1,17 @@
1
+ import logging
2
+ import os
3
+
4
+ from rich.logging import RichHandler
5
+
6
+ LOGGING_LEVEL = os.environ.get("LOGGING_LEVEL", logging.INFO)
7
+ LOGGING_FORMAT = os.environ.get("LOGGING_FORMAT", "%(message)s")
8
+
9
+ _is_debug = logging.getLevelName(LOGGING_LEVEL) == logging.DEBUG
10
+
11
+ logging.basicConfig(
12
+ level=LOGGING_LEVEL,
13
+ format=LOGGING_FORMAT,
14
+ handlers=[RichHandler(markup=True, show_path=_is_debug, show_time=_is_debug, show_level=_is_debug)],
15
+ )
16
+
17
+ logging.getLogger("httpx").setLevel(logging.WARNING)
@@ -0,0 +1,70 @@
1
+ import sys
2
+ import logging
3
+ import argparse
4
+
5
+ from bookworm_genai.commands.sync import sync
6
+ from bookworm_genai.commands.ask import BookmarkChain
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def main():
12
+ logger.info("[bold green]Starting Bookworm 📖")
13
+ logger.debug("Running on platform '%s'", sys.platform)
14
+
15
+ argparser = argparse.ArgumentParser(description="Bookworm - A LLM-powered bookmark search engine")
16
+
17
+ sub_parsers = argparser.add_subparsers(dest="command", help="Available commands")
18
+ sub_parsers.add_parser("sync", help="Sync the bookmark database with the latest changes")
19
+ sub_parsers.add_parser("ask", help="Search for a bookmark")
20
+
21
+ args = argparser.parse_args()
22
+
23
+ logger.debug("Arguments: %s", args)
24
+
25
+ if args.command == "sync":
26
+ sync()
27
+
28
+ elif args.command == "ask":
29
+ logger.info("What would you like to search for?")
30
+ query = input("> ")
31
+
32
+ logger.debug("query: %s", query)
33
+
34
+ with BookmarkChain() as bookmark_chain:
35
+ if not bookmark_chain.is_valid():
36
+ logger.debug("bookmark chain is not valid, exiting early.")
37
+ return
38
+
39
+ bookmarks = bookmark_chain.ask(query)
40
+
41
+ if not bookmarks.bookmarks:
42
+ logger.info("""
43
+ No bookmarks found for the query 🙁. Please ensure you have performed a "bookworm sync" to update the database
44
+ and the query is relevant to the bookmarks stored.
45
+ """)
46
+ return
47
+
48
+ for index, bookmark in enumerate(bookmarks.bookmarks):
49
+ if logger.isEnabledFor(logging.DEBUG):
50
+ # also shows the source of the bookmark
51
+ logger.info(f"[green][{index}] [/] {bookmark.title} - [link={bookmark.url}]{bookmark.url}[/link] ({bookmark.source})")
52
+ else:
53
+ logger.info(f"[green][{index}] [/] {bookmark.title} - [link={bookmark.url}]{bookmark.url}[/link]")
54
+
55
+ logger.info("Press a number to open the bookmark:")
56
+ while True:
57
+ try:
58
+ raw_input = input("> ")
59
+ selected_index = int(raw_input)
60
+ bookmarks.bookmarks[selected_index].open()
61
+
62
+ break
63
+ except ValueError:
64
+ logger.warning(f"Invalid input: '{raw_input}'. Please enter a number.")
65
+ except IndexError:
66
+ logger.warning(f"Invalid index: '{selected_index}'. Please select a valid index.")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
@@ -0,0 +1,86 @@
1
+ import os
2
+ import logging
3
+
4
+ import duckdb
5
+ from langchain_community.vectorstores import DuckDB as DuckDBVectorStore
6
+ from langchain_openai import ChatOpenAI, AzureChatOpenAI
7
+ from langchain_core.prompts import ChatPromptTemplate
8
+ from langchain_core.runnables import RunnablePassthrough
9
+ from langchain_core.language_models.chat_models import BaseChatModel
10
+
11
+ from bookworm_genai.models import Bookmarks
12
+ from bookworm_genai.storage import full_database_path, _get_embedding_store
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ _system_message = """
18
+ You have knowledge about all the browser bookmarks stored by an individual.
19
+ When a user asks a question, you should be able to search the bookmarks and return the most relevant bookmark title and URL.
20
+ It could be multiple bookmarks.
21
+ If you don't have anything in the context then return empty list
22
+
23
+ The bookmarks available are from the context:
24
+ {context}
25
+ """
26
+
27
+
28
+ class BookmarkChain:
29
+ def __init__(self):
30
+ logger.debug("Connecting to vector database at: %s", full_database_path)
31
+ self._duckdb_connection = duckdb.connect(full_database_path, read_only=False)
32
+ self.vector_store = DuckDBVectorStore(connection=self._duckdb_connection, embedding=_get_embedding_store())
33
+
34
+ llm = _get_llm()
35
+ llm = llm.with_structured_output(Bookmarks)
36
+
37
+ prompt = ChatPromptTemplate.from_messages([("system", _system_message), ("human", "{query}")])
38
+
39
+ self.chain = {"context": self.vector_store.as_retriever(), "query": RunnablePassthrough()} | prompt | llm
40
+
41
+ def ask(self, query: str) -> Bookmarks:
42
+ logger.debug("Searching for bookmarks with query: %s", query)
43
+
44
+ return self.chain.invoke(query)
45
+
46
+ def is_valid(self) -> bool:
47
+ res = self._duckdb_connection.execute("SELECT COUNT(*) FROM embeddings").fetchall()
48
+
49
+ try:
50
+ res = res[0][0]
51
+ except IndexError:
52
+ logger.warning("validation check failed due to unexpected response from the database.")
53
+ logger.debug("Raw DuckDB Response: %s", res)
54
+
55
+ return False
56
+
57
+ if res == 0:
58
+ logger.warning("No bookmarks were found in database. Please ensure you run 'bookworm sync' before asking questions")
59
+ return False
60
+ else:
61
+ return True
62
+
63
+ def __enter__(self):
64
+ return self
65
+
66
+ def __exit__(self, exc_type, exc_val, exc_tb):
67
+ logger.debug("Closing DuckDB connection")
68
+
69
+ self._duckdb_connection.close()
70
+
71
+
72
+ def _get_llm() -> BaseChatModel:
73
+ kwargs = {
74
+ "temperature": 0.0,
75
+ }
76
+
77
+ if os.environ.get("OPENAI_API_KEY"):
78
+ # https://api.python.langchain.com/en/latest/chat_models/langchain_openai.chat_models.base.ChatOpenAI.html
79
+ return ChatOpenAI(**kwargs)
80
+
81
+ elif os.environ.get("AZURE_OPENAI_API_KEY"):
82
+ # https://api.python.langchain.com/en/latest/chat_models/langchain_openai.chat_models.azure.AzureChatOpenAI.html
83
+ return AzureChatOpenAI(**kwargs)
84
+
85
+ else:
86
+ raise ValueError("No OpenAI API key found in environment variables")
@@ -0,0 +1,39 @@
1
+ import os
2
+ import sys
3
+ import logging
4
+
5
+ from langchain_community.document_loaders import JSONLoader
6
+
7
+ from bookworm_genai.integrations import browsers
8
+ from bookworm_genai.storage import store_documents
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def sync():
14
+ docs = []
15
+
16
+ for browser, config in browsers.items():
17
+ try:
18
+ platform_config = config[sys.platform]
19
+ except KeyError:
20
+ logger.warning(f"Platform {sys.platform} not supported for browser {browser}")
21
+ continue
22
+ else:
23
+ expanded_path = os.path.expanduser(platform_config["bookmark_file_path"])
24
+
25
+ jq_command = """
26
+ [.roots.bookmark_bar.children, .roots.other.children] |
27
+ flatten |
28
+ .. |
29
+ objects |
30
+ select(.type == "url")
31
+ """
32
+
33
+ logger.info("Loading bookmarks from %s", expanded_path)
34
+ bookmark_bar = JSONLoader(expanded_path, jq_command, text_content=False)
35
+ docs.extend(bookmark_bar.lazy_load())
36
+
37
+ logger.debug(f"{len(docs)} Bookmarks loaded")
38
+
39
+ store_documents(docs)
@@ -0,0 +1,26 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Browser(str, Enum):
5
+ BRAVE = "brave"
6
+ CHROME = "chrome"
7
+
8
+
9
+ # Configuration for various browsers and details about them
10
+ # The bookmark_file_path is the path to the bookmarks file for the browsers, in order for it to be used it must be used in conjunction with
11
+ # os.path.expanduser as it may contain environment variables
12
+ #
13
+ # The platform configuration is keyed off the values from https://docs.python.org/3/library/sys.html#sys.platform
14
+ #
15
+ browsers = {
16
+ Browser.BRAVE: {
17
+ "linux": {"bookmark_file_path": "~/.config/BraveSoftware/Brave-Browser/Default/Bookmarks"},
18
+ "win32": {},
19
+ "darwin": {},
20
+ },
21
+ Browser.CHROME: {
22
+ "linux": {"bookmark_file_path": "~/.config/google-chrome/Default/Bookmarks"},
23
+ "win32": {},
24
+ "darwin": {},
25
+ },
26
+ }
@@ -0,0 +1,36 @@
1
+ import sys
2
+ import subprocess
3
+ import logging
4
+
5
+ from langchain_core.pydantic_v1 import BaseModel, Field
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class Bookmark(BaseModel):
11
+ """
12
+ A bookmark to a website
13
+ """
14
+
15
+ title: str = Field(description="The title of the bookmark")
16
+ url: str = Field(description="The URL of the bookmark")
17
+ source: str = Field(description="The source of the bookmark")
18
+
19
+ def open(self):
20
+ if sys.platform == "win32":
21
+ subprocess.Popen(["start", self.url], shell=True)
22
+ elif sys.platform == "darwin":
23
+ subprocess.Popen(["open", self.url])
24
+ elif sys.platform == "linux":
25
+ subprocess.Popen(["xdg-open", self.url])
26
+ else:
27
+ logger.warning(f'Platform "{sys.platform}" not supported. Printing URL instead')
28
+ logger.info(self.url)
29
+
30
+
31
+ class Bookmarks(BaseModel):
32
+ """
33
+ A list of bookmarks
34
+ """
35
+
36
+ bookmarks: list[Bookmark] = Field(description="A list of bookmarks")
@@ -0,0 +1,43 @@
1
+ import os
2
+ import duckdb
3
+ import logging
4
+
5
+ from platformdirs import PlatformDirs
6
+ from langchain_community.vectorstores import DuckDB as DuckDBVectorStore
7
+ from langchain_core.documents import Document
8
+ from langchain_core.embeddings.embeddings import Embeddings
9
+ from langchain_openai.embeddings import OpenAIEmbeddings, AzureOpenAIEmbeddings
10
+
11
+ appdirs = PlatformDirs("bookworm", "bookworm")
12
+ database_name = "bookmarks.duckdb"
13
+ full_database_path = os.path.join(appdirs.user_data_dir, database_name)
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def store_documents(docs: list[Document]):
19
+ logger.debug(f"creating folder {appdirs.user_data_dir}")
20
+ os.makedirs(appdirs.user_data_dir, exist_ok=True)
21
+
22
+ embeddings = _get_embedding_store()
23
+
24
+ logger.info(f"vectorizing and storing {len(docs)} documents into {full_database_path}")
25
+ with duckdb.connect(full_database_path) as conn:
26
+ logger.debug("dropping existing embeddings table if exists")
27
+ conn.execute("DROP TABLE IF EXISTS embeddings")
28
+
29
+ logger.debug("loading documents")
30
+ DuckDBVectorStore.from_documents(docs, embeddings, connection=conn)
31
+
32
+
33
+ def _get_embedding_store() -> Embeddings:
34
+ if os.environ.get("AZURE_OPENAI_API_KEY"):
35
+ logger.debug("Using Azure OpenAI Embeddings")
36
+ return AzureOpenAIEmbeddings()
37
+
38
+ elif os.environ.get("OPENAI_API_KEY"):
39
+ logger.debug("Using OpenAI Embeddings")
40
+ return OpenAIEmbeddings()
41
+
42
+ else:
43
+ raise ValueError("No OpenAI API key found in environment variables")
@@ -0,0 +1,33 @@
1
+ [tool.poetry]
2
+ name = "bookworm_genai"
3
+ version = "0.1.1b3"
4
+ description = "Bookworm - A LLM-powered bookmark search engine"
5
+ authors = ["kiran94"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.9"
10
+ langchain = "^0.2.12"
11
+ langchain-community = "^0.2.11"
12
+ langchain-openai = "^0.1.20"
13
+ jq = "^1.7.0"
14
+ duckdb = "^1.0.0"
15
+ rich = "^13.7.1"
16
+ platformdirs = "^4.2.2"
17
+ pandas = "^2.2.2"
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ pytest = "^8.3.2"
21
+ ruff = "^0.5.6"
22
+ pytest-cov = "^5.0.0"
23
+ pytest-github-actions-annotate-failures = "^0.2.0"
24
+
25
+ [build-system]
26
+ requires = ["poetry-core"]
27
+ build-backend = "poetry.core.masonry.api"
28
+
29
+ [tool.poetry.scripts]
30
+ bookworm = 'bookworm_genai.__main__:main'
31
+
32
+ [tool.ruff]
33
+ line-length = 160