lagerbaer commited on
Commit
507d13c
·
1 Parent(s): cb82b78

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ 2023-annual-report-e.pdf filter=lfs diff=lfs merge=lfs -text
37
+ ar2022-Complete-Report.pdf filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
161
+ .python-version
2023-annual-report-e.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:16f277a17bc35015f6f06ac8a33aace39fe9ead4221f9b5cba9a9d47a6f74d49
3
+ size 8968030
README.md CHANGED
@@ -1,12 +1,8 @@
1
  ---
2
- title: Secsplorer
3
- emoji: 🏃
4
- colorFrom: green
5
- colorTo: purple
6
  sdk: gradio
7
  sdk_version: 4.13.0
8
- app_file: app.py
9
- pinned: false
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: secsplorer
3
+ app_file: simple_script.py
 
 
4
  sdk: gradio
5
  sdk_version: 4.13.0
 
 
6
  ---
7
+ # secsplorer
8
+ A RAG ChatBot POC that works with a company's annual reports
ar2022-Complete-Report.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cdef48e9d8f91f8565ad77636eb75fd19a32dd53b82e9929f29b45557481d6bb
3
+ size 7011710
docs/milestones.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Milestone Plan
2
+
3
+ Initial thoughts:
4
+
5
+ The Cohere docs already explain how to create a RAG chatbot on your own machine. To make something awesome, it should come with a decent UI. Can be simple Gradio or Streamlit for now, but it should be there.
6
+
7
+ Let's say the final goal is to have a bot that can answer questions based on TD's last 5 annual reports. One thing to note here is that makes the document situation static, whereas a company's true knowledge base will be dynamic. Anyway, for a POC it should be fine.
8
+
9
+ -> Final goal is a website that displays a chatbot which you can ask questions about TD. How do we get there in a way that is fast and efficient?
10
+
11
+ 1. Get Cohere's _simple_ example running _locally_. Involves basic understanding, and having an API key.
12
+ 2. Put the simple example into a WebUI (Gradio?)
13
+ 3. Now rejig the simple example so it can work with PDFs
14
+ 4. Host it somewhere!
15
+
16
+ Obviously, we could switch around 2 and 3. I'm not sure if either is better. Having the GUI first means we get to something sooner that's end-to-end. Doing the PDFs first means we tackle a slightly more difficult tech problem sooner. 🤷‍♂️.
17
+
18
+ I'm leaving the hosting for last in case the prior steps inform strongly how that hosting should happen in the end.
gradio_tests.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import gradio as gr
3
+
4
+
5
+ def slow_echo(message, history):
6
+ for i in range(len(message)):
7
+ time.sleep(0.3)
8
+ yield "You typed: " + message[: i + 1]
9
+
10
+
11
+ gr.ChatInterface(slow_echo).launch()
modal_gradio_test.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modal import Stub, Image, asgi_app
2
+
3
+ from fastapi import FastAPI
4
+
5
+
6
+ image = Image.debian_slim("3.11").pip_install(
7
+ "gradio",
8
+ )
9
+
10
+ stub = Stub("secsplorer", image=image)
11
+
12
+ web_app = FastAPI()
13
+
14
+
15
+ @stub.function()
16
+ @asgi_app()
17
+ def fastapi_app():
18
+ import gradio as gr
19
+ from gradio.routes import mount_gradio_app
20
+
21
+ def chat_function(message, history):
22
+ yield "Foo!"
23
+
24
+ interface = gr.ChatInterface(chat_function)
25
+
26
+ return mount_gradio_app(app=web_app, blocks=interface, path="/")
modal_script.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modal import Stub, Image, Secret, asgi_app, method
2
+ from urllib.request import urlretrieve
3
+ from fastapi import FastAPI
4
+ from typing import List, Dict
5
+
6
+
7
+ image = Image.debian_slim("3.11").pip_install(
8
+ "cohere",
9
+ "gradio",
10
+ "pinecone-client",
11
+ )
12
+
13
+ stub = Stub("secsplorer", image=image)
14
+
15
+ web_app = FastAPI()
16
+
17
+
18
+ @stub.function(
19
+ secrets=[Secret.from_name("cohere-api-key"), Secret.from_name("pinecone-api-key")]
20
+ )
21
+ @asgi_app()
22
+ def fastapi_app():
23
+ import cohere
24
+ import pinecone
25
+ import os
26
+ import uuid
27
+
28
+ import gradio as gr
29
+ from gradio.routes import mount_gradio_app
30
+
31
+ # print("Connecting to cohere client")
32
+ # co = cohere.Client(os.environ["COHERE_API_KEY"])
33
+ # print("Done")
34
+ # # pinecone.init(api_key=os.environ["PINECONE_API_KEY"], environment="gcp-starter")
35
+ # # index = pinecone.Index(index_name="td-sec-embeddings")
36
+ # index = None
37
+
38
+ # def retrieve(
39
+ # index: pinecone.Index, query: str, co: cohere.Client
40
+ # ) -> List[Dict[str, str]]:
41
+ # """
42
+ # Retrieves documents based on the given query.
43
+
44
+ # Parameters:
45
+ # query (str): The query to retrieve documents for.
46
+
47
+ # Returns:
48
+ # List[Dict[str, str]]: A list of dictionaries representing the retrieved documents, with 'title', 'snippet', and 'url' keys.
49
+ # """
50
+ # docs_retrieved = []
51
+
52
+ # print(f"Calling retrieve for '{query}'")
53
+ # print("Embedding the query")
54
+ # query_emb = co.embed(
55
+ # texts=[query], model="embed-english-v3.0", input_type="search_query"
56
+ # ).embeddings
57
+
58
+ # print("Querying pinecone")
59
+ # res = index.query(query_emb, top_k=10, include_metadata=True)
60
+ # print("Preparing to rerank")
61
+ # docs_to_rerank = [match["metadata"] for match in res["matches"]]
62
+
63
+ # rerank_results = co.rerank(
64
+ # query=query,
65
+ # documents=docs_to_rerank,
66
+ # top_n=3,
67
+ # model="rerank-english-v2.0",
68
+ # )
69
+
70
+ # docs_retrieved = []
71
+ # for hit in rerank_results:
72
+ # docs_retrieved.append(docs_to_rerank[hit.index])
73
+
74
+ # print("Returning retrieved docs")
75
+ # return docs_retrieved
76
+
77
+ # class Chatbot:
78
+ # def __init__(self, co: cohere.Client, index: pinecone.Index):
79
+ # self.index = index
80
+ # self.conversation_id = str(uuid.uuid4())
81
+ # self.co = co
82
+
83
+ # def generate_response(self, message: str):
84
+ # """
85
+ # Generates a response to the user's message.
86
+
87
+ # Parameters:
88
+ # message (str): The user's message.
89
+
90
+ # Yields:
91
+ # Event: A response event generated by the chatbot.
92
+
93
+ # Returns:
94
+ # List[Dict[str, str]]: A list of dictionaries representing the retrieved documents.
95
+
96
+ # """
97
+
98
+ # # Generate search queries (if any)
99
+ # response = self.co.chat(message=message, search_queries_only=True)
100
+
101
+ # # If there are search queries, retrieve documents and respond
102
+ # if response.search_queries:
103
+ # print("Retrieving information")
104
+
105
+ # documents = self.retrieve_docs(response)
106
+
107
+ # response = self.co.chat(
108
+ # message=message,
109
+ # documents=documents,
110
+ # conversation_id=self.conversation_id,
111
+ # stream=True,
112
+ # )
113
+ # for event in response:
114
+ # yield event
115
+
116
+ # # If there is no search query, directly respond
117
+ # else:
118
+ # response = self.co.chat(
119
+ # message=message, conversation_id=self.conversation_id, stream=True
120
+ # )
121
+ # for event in response:
122
+ # yield event
123
+
124
+ # def retrieve_docs(self, response) -> List[Dict[str, str]]:
125
+ # """
126
+ # Retrieves documents based on the search queries in the response.
127
+
128
+ # Parameters:
129
+ # response: The response object containing search queries.
130
+
131
+ # Returns:
132
+ # List[Dict[str, str]]: A list of dictionaries representing the retrieved documents.
133
+
134
+ # """
135
+ # # Get the query(s)
136
+
137
+ # queries = []
138
+ # for search_query in response.search_queries:
139
+ # queries.append(search_query["text"])
140
+
141
+ # # Retrieve documents for each query
142
+ # retrieved_docs = []
143
+ # for query in queries:
144
+ # retrieved_docs.extend(retrieve(self.index, query, self.co))
145
+
146
+ # return retrieved_docs
147
+
148
+ # chatbot = Chatbot(co, index)
149
+
150
+ def chat_function(message, history):
151
+ return "Foo!"
152
+ # flag = False
153
+ # reply = ""
154
+ # for event in chatbot.generate_response(message):
155
+ # if event.event_type == "text-generation":
156
+ # reply += str(event.text)
157
+ # yield reply
158
+
159
+ # # Citations
160
+ # if event.event_type == "citation-generation":
161
+ # if not flag:
162
+ # reply += "\n\nCITATIONS:\n\n"
163
+ # yield reply
164
+ # flag = True
165
+ # reply += str(event.citations) + "\n"
166
+ # yield reply
167
+
168
+ interface = gr.ChatInterface(chat_function)
169
+
170
+ print("All ready!")
171
+ return mount_gradio_app(app=web_app, blocks=interface, path="/")
requirements.txt ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==23.2.1
2
+ aiohttp==3.9.1
3
+ aiosignal==1.3.1
4
+ altair==5.2.0
5
+ annotated-types==0.6.0
6
+ anyio==4.2.0
7
+ attrs==23.2.0
8
+ backoff==2.2.1
9
+ beautifulsoup4==4.12.2
10
+ certifi==2023.11.17
11
+ chardet==5.2.0
12
+ charset-normalizer==3.3.2
13
+ click==8.1.7
14
+ cohere==4.40
15
+ colorama==0.4.6
16
+ contourpy==1.2.0
17
+ cycler==0.12.1
18
+ dataclasses-json==0.6.3
19
+ emoji==2.9.0
20
+ fastapi==0.108.0
21
+ fastavro==1.9.2
22
+ ffmpy==0.3.1
23
+ filelock==3.13.1
24
+ filetype==1.2.0
25
+ fonttools==4.47.0
26
+ frozenlist==1.4.1
27
+ fsspec==2023.12.2
28
+ gradio==4.13.0
29
+ gradio_client==0.8.0
30
+ h11==0.14.0
31
+ hnswlib==0.8.0
32
+ httpcore==1.0.2
33
+ httpx==0.26.0
34
+ huggingface-hub==0.20.2
35
+ idna==3.6
36
+ importlib-metadata==6.11.0
37
+ importlib-resources==6.1.1
38
+ Jinja2==3.1.2
39
+ joblib==1.3.2
40
+ jsonpath-python==1.0.6
41
+ jsonschema==4.20.0
42
+ jsonschema-specifications==2023.12.1
43
+ kiwisolver==1.4.5
44
+ langdetect==1.0.9
45
+ lxml==5.0.1
46
+ markdown-it-py==3.0.0
47
+ MarkupSafe==2.1.3
48
+ marshmallow==3.20.1
49
+ matplotlib==3.8.2
50
+ mdurl==0.1.2
51
+ multidict==6.0.4
52
+ mypy-extensions==1.0.0
53
+ nltk==3.8.1
54
+ numpy==1.26.3
55
+ orjson==3.9.10
56
+ packaging==23.2
57
+ pandas==2.1.4
58
+ pillow==10.2.0
59
+ pydantic==2.5.3
60
+ pydantic_core==2.14.6
61
+ pydub==0.25.1
62
+ Pygments==2.17.2
63
+ PyMuPDF==1.23.8
64
+ PyMuPDFb==1.23.7
65
+ pyparsing==3.1.1
66
+ python-dateutil==2.8.2
67
+ python-dotenv==1.0.0
68
+ python-iso639==2024.1.2
69
+ python-magic==0.4.27
70
+ python-multipart==0.0.6
71
+ pytz==2023.3.post1
72
+ PyYAML==6.0.1
73
+ rapidfuzz==3.6.1
74
+ referencing==0.32.1
75
+ regex==2023.12.25
76
+ requests==2.31.0
77
+ rich==13.7.0
78
+ rpds-py==0.16.2
79
+ semantic-version==2.10.0
80
+ shellingham==1.5.4
81
+ six==1.16.0
82
+ sniffio==1.3.0
83
+ soupsieve==2.5
84
+ starlette==0.32.0.post1
85
+ tabulate==0.9.0
86
+ tomlkit==0.12.0
87
+ toolz==0.12.0
88
+ tqdm==4.66.1
89
+ typer==0.9.0
90
+ typing-inspect==0.9.0
91
+ typing_extensions==4.9.0
92
+ tzdata==2023.4
93
+ unstructured==0.11.8
94
+ unstructured-client==0.15.1
95
+ urllib3==2.1.0
96
+ uvicorn==0.25.0
97
+ websockets==11.0.3
98
+ wrapt==1.16.0
99
+ yarl==1.9.4
100
+ zipp==3.17.0
101
+ modal
secsplorer/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .documents import Documents
2
+ from .chatbot import Chatbot
3
+
4
+ __all__ = [Chatbot, Documents]
secsplorer/chatbot.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import cohere
3
+ from secsplorer import Documents
4
+ from typing import List, Dict
5
+
6
+
7
+ class Chatbot:
8
+ def __init__(self, co: cohere.Client, docs: Documents):
9
+ self.docs = docs
10
+ self.conversation_id = str(uuid.uuid4())
11
+ self.co = co
12
+
13
+ def generate_response(self, message: str):
14
+ """
15
+ Generates a response to the user's message.
16
+
17
+ Parameters:
18
+ message (str): The user's message.
19
+
20
+ Yields:
21
+ Event: A response event generated by the chatbot.
22
+
23
+ Returns:
24
+ List[Dict[str, str]]: A list of dictionaries representing the retrieved documents.
25
+
26
+ """
27
+
28
+ # Generate search queries (if any)
29
+ response = self.co.chat(message=message, search_queries_only=True)
30
+
31
+ # If there are search queries, retrieve documents and respond
32
+ if response.search_queries:
33
+ print("Retrieving information...")
34
+
35
+ documents = self.retrieve_docs(response)
36
+
37
+ response = self.co.chat(
38
+ message=message,
39
+ documents=documents,
40
+ conversation_id=self.conversation_id,
41
+ stream=True,
42
+ )
43
+ for event in response:
44
+ yield event
45
+
46
+ # If there is no search query, directly respond
47
+ else:
48
+ response = self.co.chat(
49
+ message=message, conversation_id=self.conversation_id, stream=True
50
+ )
51
+ for event in response:
52
+ yield event
53
+
54
+ def retrieve_docs(self, response) -> List[Dict[str, str]]:
55
+ """
56
+ Retrieves documents based on the search queries in the response.
57
+
58
+ Parameters:
59
+ response: The response object containing search queries.
60
+
61
+ Returns:
62
+ List[Dict[str, str]]: A list of dictionaries representing the retrieved documents.
63
+
64
+ """
65
+ # Get the query(s)
66
+ queries = []
67
+ for search_query in response.search_queries:
68
+ queries.append(search_query["text"])
69
+
70
+ # Retrieve documents for each query
71
+ retrieved_docs = []
72
+ for query in queries:
73
+ retrieved_docs.extend(self.docs.retrieve(query, self.co))
74
+
75
+ return retrieved_docs
secsplorer/documents.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict
2
+ from unstructured.partition.pdf import partition_pdf
3
+ from unstructured.chunking.title import chunk_by_title
4
+ import hnswlib
5
+ import cohere
6
+
7
+
8
+ class Documents:
9
+ def __init__(self, co: cohere.Client, sources: List[Dict[str, str]]):
10
+ self.co = co
11
+ self.sources = sources
12
+ self.docs = []
13
+ self.docs_embs = []
14
+ self.retrieve_top_k = 10
15
+ self.rerank_top_k = 3
16
+ self.load()
17
+ self.embed(co)
18
+ self.index()
19
+
20
+ def load(self) -> None:
21
+ """
22
+ Loads the documents from the sources and chunks the HTML content.
23
+ """
24
+ print("Loading documents...")
25
+
26
+ for source in self.sources:
27
+ # elements = partition_html(url=source["url"])
28
+ # elements = partition_pdf(url=source["url"])
29
+ elements = partition_pdf(filename=source["filename"])
30
+ chunks = chunk_by_title(elements)
31
+ for chunk in chunks:
32
+ self.docs.append(
33
+ {
34
+ "title": source["title"],
35
+ "text": str(chunk),
36
+ "url": source["url"],
37
+ }
38
+ )
39
+
40
+ def embed(self) -> None:
41
+ """
42
+ Embeds the documents using the Cohere API.
43
+ """
44
+ print("Embedding documents...")
45
+
46
+ batch_size = 90
47
+ self.docs_len = len(self.docs)
48
+
49
+ for i in range(0, self.docs_len, batch_size):
50
+ batch = self.docs[i : min(i + batch_size, self.docs_len)]
51
+ texts = [item["text"] for item in batch]
52
+ docs_embs_batch = self.co.embed(
53
+ texts=texts, model="embed-english-v3.0", input_type="search_document"
54
+ ).embeddings
55
+ self.docs_embs.extend(docs_embs_batch)
56
+
57
+ def index(self) -> None:
58
+ """
59
+ Indexes the documents for efficient retrieval.
60
+ """
61
+ print("Indexing documents...")
62
+
63
+ self.index = hnswlib.Index(space="ip", dim=1024)
64
+ self.index.init_index(max_elements=self.docs_len, ef_construction=512, M=64)
65
+ self.index.add_items(self.docs_embs, list(range(len(self.docs_embs))))
66
+
67
+ print(f"Indexing complete with {self.index.get_current_count()} documents.")
68
+
69
+ def retrieve(self, query: str) -> List[Dict[str, str]]:
70
+ """
71
+ Retrieves documents based on the given query.
72
+
73
+ Parameters:
74
+ query (str): The query to retrieve documents for.
75
+
76
+ Returns:
77
+ List[Dict[str, str]]: A list of dictionaries representing the retrieved documents, with 'title', 'snippet', and 'url' keys.
78
+ """
79
+ docs_retrieved = []
80
+ query_emb = self.co.embed(
81
+ texts=[query], model="embed-english-v3.0", input_type="search_query"
82
+ ).embeddings
83
+
84
+ doc_ids = self.index.knn_query(query_emb, k=self.retrieve_top_k)[0][0]
85
+
86
+ docs_to_rerank = []
87
+ for doc_id in doc_ids:
88
+ docs_to_rerank.append(self.docs[doc_id]["text"])
89
+
90
+ rerank_results = self.co.rerank(
91
+ query=query,
92
+ documents=docs_to_rerank,
93
+ top_n=self.rerank_top_k,
94
+ model="rerank-english-v2.0",
95
+ )
96
+
97
+ doc_ids_reranked = []
98
+ for result in rerank_results:
99
+ doc_ids_reranked.append(doc_ids[result.index])
100
+
101
+ for doc_id in doc_ids_reranked:
102
+ docs_retrieved.append(
103
+ {
104
+ "title": self.docs[doc_id]["title"],
105
+ "text": self.docs[doc_id]["text"],
106
+ "url": self.docs[doc_id]["url"],
107
+ }
108
+ )
109
+
110
+ return docs_retrieved
setup.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="secsplorer",
5
+ version="0.1",
6
+ packages=find_packages(),
7
+ author="Clemens Adolphs",
8
+ author_email="clemens.adolphs@gmail.com",
9
+ description="The private python package for the secsplorer",
10
+ )
simple_script.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cohere
2
+ import os
3
+ import pinecone
4
+ import uuid
5
+
6
+ from typing import List, Dict
7
+
8
+ # from unstructured.chunking.title import chunk_by_title
9
+ # from unstructured.partition.pdf import partition_pdf
10
+ from dotenv import load_dotenv
11
+
12
+
13
+ load_dotenv()
14
+
15
+ co = cohere.Client(os.environ["COHERE_API_KEY"])
16
+
17
+ pinecone.init(api_key=os.environ["PINECONE_API_KEY"], environment="gcp-starter")
18
+
19
+ index = pinecone.Index("td-sec-embeddings")
20
+
21
+ from typing import List, Dict
22
+
23
+ # from unstructured.partition.pdf import partition_pdf
24
+ # from unstructured.chunking.title import chunk_by_title
25
+
26
+ import cohere
27
+
28
+ sources = [
29
+ {
30
+ "title": "2023",
31
+ "url": "https://www.td.com/content/dam/tdcom/canada/about-td/pdf/quarterly-results/2023/2023-annual-report-e.pdf",
32
+ "filename": "/Users/clemensadolphs/git-personal/secsplorer/2023-annual-report-e.pdf",
33
+ },
34
+ # {
35
+ # "title": "2022",
36
+ # "url": "https://www.td.com/document/PDF/ar2022/ar2022-Complete-Report.pdf",
37
+ # "filename": "/Users/clemensadolphs/git-personal/secsplorer/2023-annual-report-e.pdf",
38
+ # },
39
+ ]
40
+
41
+
42
+ def load() -> List[Dict[str, str]]:
43
+ """
44
+ Loads the documents from the sources and chunks the HTML content.
45
+ """
46
+ print("Loading documents...")
47
+ docs = []
48
+ for source in sources:
49
+ elements = partition_pdf(filename=source["filename"])
50
+ chunks = chunk_by_title(elements)
51
+ for chunk in chunks:
52
+ docs.append(
53
+ {
54
+ "title": source["title"],
55
+ "text": str(chunk),
56
+ "url": source["url"],
57
+ }
58
+ )
59
+ return docs
60
+
61
+
62
+ def embed(docs: List[Dict[str, str]]) -> List[List[float]]:
63
+ """
64
+ Embeds the documents using the Cohere API.
65
+ """
66
+ print("Embedding documents...")
67
+
68
+ batch_size = 90
69
+ docs_len = len(docs)
70
+ docs_embs = []
71
+
72
+ for i in range(0, docs_len, batch_size):
73
+ batch = docs[i : min(i + batch_size, docs_len)]
74
+ texts = [item["text"] for item in batch]
75
+ docs_embs_batch = co.embed(
76
+ texts=texts, model="embed-english-v3.0", input_type="search_document"
77
+ ).embeddings
78
+ docs_embs.extend(docs_embs_batch)
79
+ return docs_embs
80
+
81
+
82
+ def update_index(
83
+ index: pinecone.Index, docs: List[Dict[str, str]], docs_embs: List[List[float]]
84
+ ) -> None:
85
+ """
86
+ Indexes the documents for efficient retrieval.
87
+ """
88
+ batch_size = 100
89
+
90
+ ids = [str(i) for i in range(len(docs))]
91
+
92
+ to_upsert = list(zip(ids, docs_embs, docs))
93
+
94
+ for i in range(0, len(docs), batch_size):
95
+ i_end = min(i + batch_size, len(docs))
96
+ index.upsert(vectors=to_upsert[i:i_end])
97
+
98
+
99
+ def retrieve(index: pinecone.Index, query: str) -> List[Dict[str, str]]:
100
+ """
101
+ Retrieves documents based on the given query.
102
+
103
+ Parameters:
104
+ query (str): The query to retrieve documents for.
105
+
106
+ Returns:
107
+ List[Dict[str, str]]: A list of dictionaries representing the retrieved documents, with 'title', 'snippet', and 'url' keys.
108
+ """
109
+ docs_retrieved = []
110
+
111
+ print(f"Calling retrieve for '{query}'")
112
+ print("Embedding the query")
113
+ query_emb = co.embed(
114
+ texts=[query], model="embed-english-v3.0", input_type="search_query"
115
+ ).embeddings
116
+
117
+ print("Querying pinecone")
118
+ res = index.query(query_emb, top_k=10, include_metadata=True)
119
+ print("Preparing to rerank")
120
+ docs_to_rerank = [match["metadata"] for match in res["matches"]]
121
+
122
+ rerank_results = co.rerank(
123
+ query=query,
124
+ documents=docs_to_rerank,
125
+ top_n=3,
126
+ model="rerank-english-v2.0",
127
+ )
128
+
129
+ docs_retrieved = []
130
+ for hit in rerank_results:
131
+ docs_retrieved.append(docs_to_rerank[hit.index])
132
+
133
+ print("Returning retrieved docs")
134
+ return docs_retrieved
135
+
136
+
137
+ # docs = load()
138
+ # docs_embeds = embed(docs)
139
+ # update_index(index, docs=docs, docs_embs=docs_embeds)
140
+
141
+
142
+ class Chatbot:
143
+ def __init__(self, co: cohere.Client, index: pinecone.Index):
144
+ self.index = index
145
+ self.conversation_id = str(uuid.uuid4())
146
+ self.co = co
147
+
148
+ def generate_response(self, message: str):
149
+ """
150
+ Generates a response to the user's message.
151
+
152
+ Parameters:
153
+ message (str): The user's message.
154
+
155
+ Yields:
156
+ Event: A response event generated by the chatbot.
157
+
158
+ Returns:
159
+ List[Dict[str, str]]: A list of dictionaries representing the retrieved documents.
160
+
161
+ """
162
+
163
+ # Generate search queries (if any)
164
+ response = self.co.chat(message=message, search_queries_only=True)
165
+
166
+ # If there are search queries, retrieve documents and respond
167
+ if response.search_queries:
168
+ print("Retrieving information...")
169
+
170
+ documents = self.retrieve_docs(response)
171
+ print(f"Generating response with documents {documents}")
172
+ response = self.co.chat(
173
+ message=message,
174
+ documents=documents,
175
+ conversation_id=self.conversation_id,
176
+ stream=True,
177
+ )
178
+ for event in response:
179
+ yield event
180
+
181
+ # If there is no search query, directly respond
182
+ else:
183
+ response = self.co.chat(
184
+ message=message, conversation_id=self.conversation_id, stream=True
185
+ )
186
+ for event in response:
187
+ yield event
188
+
189
+ def retrieve_docs(self, response) -> List[Dict[str, str]]:
190
+ """
191
+ Retrieves documents based on the search queries in the response.
192
+
193
+ Parameters:
194
+ response: The response object containing search queries.
195
+
196
+ Returns:
197
+ List[Dict[str, str]]: A list of dictionaries representing the retrieved documents.
198
+
199
+ """
200
+ # Get the query(s)
201
+ print("Calling retrieve_docs")
202
+ queries = []
203
+ for search_query in response.search_queries:
204
+ queries.append(search_query["text"])
205
+ print(queries)
206
+
207
+ # Retrieve documents for each query
208
+ retrieved_docs = []
209
+ for query in queries:
210
+ retrieved_docs.extend(retrieve(self.index, query))
211
+
212
+ return retrieved_docs
213
+
214
+
215
+ import gradio as gr
216
+
217
+ chatbot = Chatbot(co, index)
218
+
219
+
220
+ def chat_function(message, history):
221
+ flag = False
222
+ reply = ""
223
+ for event in chatbot.generate_response(message):
224
+ if event.event_type == "text-generation":
225
+ reply += str(event.text)
226
+ yield reply
227
+
228
+ # Citations
229
+ if event.event_type == "citation-generation":
230
+ if not flag:
231
+ reply += "\n\nCITATIONS:\n\n"
232
+ yield reply
233
+ flag = True
234
+ reply += str(event.citations) + "\n"
235
+ yield reply
236
+
237
+
238
+ gr.ChatInterface(chat_function).launch(share=True)