Initial commit
Browse files- .gitignore +160 -0
- app.py +114 -0
- environment.yml +15 -0
- main.py +74 -0
- requirements.txt +6 -0
- utils/env.py +30 -0
- utils/functions.py +80 -0
- utils/google_serper.py +182 -0
- utils/tts.py +58 -0
- utils/weather_forecast.py +157 -0
.gitignore
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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/
|
app.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import time
|
3 |
+
|
4 |
+
import gradio as gr
|
5 |
+
import openai
|
6 |
+
from dotenv import load_dotenv, find_dotenv
|
7 |
+
from simpleaichat import AIChat
|
8 |
+
|
9 |
+
from main import weather, search, MODEL
|
10 |
+
from utils.tts import TTS, voices
|
11 |
+
|
12 |
+
load_dotenv(find_dotenv())
|
13 |
+
openai.api_key = os.getenv("OPENAI_API_KEY")
|
14 |
+
|
15 |
+
|
16 |
+
def transcribe(audio_file, state=""):
|
17 |
+
time.sleep(5)
|
18 |
+
if audio_file is None:
|
19 |
+
return None
|
20 |
+
prompt = (
|
21 |
+
"The author of this tool is Somto Muotoe. "
|
22 |
+
"Friends: Ire Ireoluwa Adedugbe, Biola Aderiye, Jelson, Raj, Akshay."
|
23 |
+
"Umm, let me think like, hmm... Okay, here's what I'm, like, "
|
24 |
+
"thinking. "
|
25 |
+
)
|
26 |
+
with open(audio_file, "rb") as f:
|
27 |
+
response = openai.Audio.transcribe("whisper-1", f, prompt=prompt)
|
28 |
+
text = response["text"]
|
29 |
+
state += text
|
30 |
+
return state, state
|
31 |
+
|
32 |
+
|
33 |
+
def chat_with_gpt(prompt, ai_state):
|
34 |
+
if ai_state is None:
|
35 |
+
params = {"temperature": 0.0, "max_tokens": 200}
|
36 |
+
system_prompt = (
|
37 |
+
"You are a confidante whose response is curt and concise."
|
38 |
+
"You can use tools to give real-time updates on weather and search the internet. "
|
39 |
+
"Answer all questions empathetically, and ALWAYS ask follow-up questions."
|
40 |
+
"Do NOT say Confidante in any response."
|
41 |
+
"You must TRUST the provided context to inform your response."
|
42 |
+
# "If a question does not make any sense, or is not factually coherent, explain why "
|
43 |
+
# "instead of answering something not correct. If you don't know the answer to a question, "
|
44 |
+
# "please don't share false information."
|
45 |
+
)
|
46 |
+
ai = AIChat(
|
47 |
+
params=params, model=MODEL, system=system_prompt, save_messages=True
|
48 |
+
)
|
49 |
+
else:
|
50 |
+
ai = ai_state
|
51 |
+
tools = [weather, search]
|
52 |
+
|
53 |
+
response = ai(prompt, tools=tools)
|
54 |
+
text_response = response["response"]
|
55 |
+
print(text_response)
|
56 |
+
return text_response, ai
|
57 |
+
|
58 |
+
|
59 |
+
def tts(text, voice_id):
|
60 |
+
# Generate audio from the text response
|
61 |
+
tts_ = TTS(voice_id)
|
62 |
+
audio_data = tts_.generate(text=text)
|
63 |
+
return audio_data
|
64 |
+
|
65 |
+
|
66 |
+
def transcribe_and_chat(audio_file, voice, history, ai_state):
|
67 |
+
if audio_file is None:
|
68 |
+
raise gr.Error("Empty audio file.")
|
69 |
+
voice_id = voices[voice]
|
70 |
+
|
71 |
+
text, text_state = transcribe(audio_file)
|
72 |
+
gpt_response, ai_state = chat_with_gpt(text, ai_state)
|
73 |
+
audio_data = tts(gpt_response, voice_id)
|
74 |
+
|
75 |
+
# Update the history with the new messages
|
76 |
+
history.append((text, gpt_response))
|
77 |
+
|
78 |
+
return history, audio_data, history, ai_state
|
79 |
+
|
80 |
+
|
81 |
+
def clear_chat(history):
|
82 |
+
# Clear the chat history
|
83 |
+
history.clear()
|
84 |
+
|
85 |
+
# Clear the chat for the AIChat object
|
86 |
+
chat_with_gpt("", ai_state=None)
|
87 |
+
|
88 |
+
return history
|
89 |
+
|
90 |
+
|
91 |
+
with gr.Blocks(title="JARVIS") as demo:
|
92 |
+
gr.Markdown(
|
93 |
+
"# Talk with GPT-4! You can get real-time weather updates, and can search Google."
|
94 |
+
)
|
95 |
+
audio_input = gr.Audio(source="microphone", type="filepath", visible=True)
|
96 |
+
gr.ClearButton(audio_input)
|
97 |
+
|
98 |
+
voice_select = gr.Radio(choices=list(voices.keys()), label="Voice", value="Bella")
|
99 |
+
history = gr.State(label="History", value=[])
|
100 |
+
ai_state = gr.State(label="AIChat", value=None)
|
101 |
+
# transcription = gr.Textbox(lines=2, label="Transcription")
|
102 |
+
chat_box = gr.Chatbot(label="Response")
|
103 |
+
response_audio = gr.Audio(label="Response Audio", autoplay=True)
|
104 |
+
gr.ClearButton(chat_box, value="Clear Chat")
|
105 |
+
# clear_chat_btn.click(clear_chat, inputs=history, outputs=history)
|
106 |
+
|
107 |
+
audio_input.stop_recording(
|
108 |
+
transcribe_and_chat,
|
109 |
+
inputs=[audio_input, voice_select, history, ai_state],
|
110 |
+
outputs=[chat_box, response_audio, history, ai_state],
|
111 |
+
)
|
112 |
+
audio_input.clear()
|
113 |
+
|
114 |
+
demo.launch(server_port=8080, share=True)
|
environment.yml
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: jarvis
|
2 |
+
channels:
|
3 |
+
- conda-forge
|
4 |
+
dependencies:
|
5 |
+
- python=3.10
|
6 |
+
- pip
|
7 |
+
- python-dotenv
|
8 |
+
- black
|
9 |
+
- pip:
|
10 |
+
- simpleaichat
|
11 |
+
- pyowm>3.0
|
12 |
+
- aiohttp
|
13 |
+
- openai
|
14 |
+
- gradio
|
15 |
+
- faster-whisper
|
main.py
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import partial
|
2 |
+
from os import getenv
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
from dotenv import find_dotenv, load_dotenv
|
6 |
+
from simpleaichat import AIChat
|
7 |
+
|
8 |
+
from utils.google_serper import GoogleSerper
|
9 |
+
from utils.weather_forecast import WeatherAPI
|
10 |
+
|
11 |
+
load_dotenv(find_dotenv(raise_error_if_not_found=True))
|
12 |
+
|
13 |
+
local_dir = Path(getenv("LOCAL_DIR"))
|
14 |
+
|
15 |
+
OPENAI_API_KEY = getenv("OPENAI_API_KEY")
|
16 |
+
MODEL = "gpt-4"
|
17 |
+
# MODEL = "gpt-3.5-turbo-16k"
|
18 |
+
# %%
|
19 |
+
|
20 |
+
|
21 |
+
# This uses the Wikipedia Search API.
|
22 |
+
# Results from it are nondeterministic, your mileage will vary.
|
23 |
+
# def search(query):
|
24 |
+
# """Search the internet."""
|
25 |
+
# wiki_matches = wikipedia_search(query, n=3)
|
26 |
+
# return {"context": ", ".join(wiki_matches), "titles": wiki_matches}
|
27 |
+
|
28 |
+
|
29 |
+
# def lookup(query):
|
30 |
+
# """Lookup more information about a topic."""
|
31 |
+
# page = wikipedia_search_lookup(query, sentences=3)
|
32 |
+
# return page
|
33 |
+
|
34 |
+
|
35 |
+
def weather(query):
|
36 |
+
"""Use this tool to get real-time temperature and weather forecast information"""
|
37 |
+
w = WeatherAPI()
|
38 |
+
weather_info = w.run(query)
|
39 |
+
return weather_info
|
40 |
+
|
41 |
+
|
42 |
+
def search(query):
|
43 |
+
"""Search the internet."""
|
44 |
+
g = GoogleSerper()
|
45 |
+
search_result = g.run(query)
|
46 |
+
return search_result
|
47 |
+
|
48 |
+
|
49 |
+
if __name__ == "__main__":
|
50 |
+
params = {"temperature": 0.1, "max_tokens": 200}
|
51 |
+
system_prompt = (
|
52 |
+
"You are an assistant that likes to use emoticons. "
|
53 |
+
"You are also curt and concise with your responses."
|
54 |
+
)
|
55 |
+
ai = AIChat(console=False, id="chat_1")
|
56 |
+
ai.load_session("chat_session_3.json", id="chat_1")
|
57 |
+
ai.get_session()
|
58 |
+
# AIChat(character="Morgan Freeman")
|
59 |
+
tools = [weather, search]
|
60 |
+
ai("can you summarize the conversation?")
|
61 |
+
ai.save_session("chat_session_3.json", format="json")
|
62 |
+
ai_with_tools = partial(ai, tools=tools)
|
63 |
+
|
64 |
+
while True:
|
65 |
+
response = ai_with_tools(input("User: "))
|
66 |
+
print(response["response"])
|
67 |
+
|
68 |
+
# ai.reset_session()
|
69 |
+
|
70 |
+
# TODO: play music, create routines for spotify and/or amazon music; play videos on youtube (i know it will just be the audio)
|
71 |
+
# use faster-whisper instead of the API
|
72 |
+
# using llama.cpp https://github.com/minimaxir/simpleaichat/pull/52
|
73 |
+
# checkout swarms: https://github.com/kyegomez/swarms
|
74 |
+
# improve chat interface: https://www.gradio.app/guides/creating-a-custom-chatbot-with-blocks
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
python-dotenv
|
2 |
+
simpleaichat
|
3 |
+
aiohttp
|
4 |
+
openai
|
5 |
+
gradio
|
6 |
+
faster-whisper
|
utils/env.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from typing import Optional, Any, Dict
|
3 |
+
|
4 |
+
from dotenv import find_dotenv, load_dotenv
|
5 |
+
|
6 |
+
load_dotenv(find_dotenv(raise_error_if_not_found=True))
|
7 |
+
|
8 |
+
|
9 |
+
def get_from_dict_or_env(
|
10 |
+
data: Dict[str, Any], key: str, env_key: str, default: Optional[str] = None
|
11 |
+
) -> str:
|
12 |
+
"""Get a value from a dictionary or an environment variable."""
|
13 |
+
if key in data and data[key]:
|
14 |
+
return data[key]
|
15 |
+
else:
|
16 |
+
return get_from_env(env_key, default=default)
|
17 |
+
|
18 |
+
|
19 |
+
def get_from_env(env_key: str, default: Optional[str] = None) -> str:
|
20 |
+
"""Get a value from a dictionary or an environment variable."""
|
21 |
+
if env_key in os.environ and os.environ[env_key]:
|
22 |
+
return os.environ[env_key]
|
23 |
+
elif default is not None:
|
24 |
+
return default
|
25 |
+
else:
|
26 |
+
raise ValueError(
|
27 |
+
f"Did not find {env_key}, please add an environment variable"
|
28 |
+
f" `{env_key}` which contains it, or pass"
|
29 |
+
f" `{env_key}` as a named parameter."
|
30 |
+
)
|
utils/functions.py
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dotenv import find_dotenv, load_dotenv
|
2 |
+
import requests
|
3 |
+
import os
|
4 |
+
import shutil
|
5 |
+
import subprocess
|
6 |
+
from pathlib import Path
|
7 |
+
from typing import Iterator, Optional
|
8 |
+
|
9 |
+
load_dotenv(find_dotenv())
|
10 |
+
|
11 |
+
|
12 |
+
def get_voices():
|
13 |
+
url = "https://api.elevenlabs.io/v1/voices"
|
14 |
+
api_key = os.getenv("ELEVEN_API_KEY")
|
15 |
+
headers = {"Accept": "application/json", "xi-api-key": api_key}
|
16 |
+
|
17 |
+
response = requests.get(url, headers=headers)
|
18 |
+
|
19 |
+
print(response.text)
|
20 |
+
return response.text
|
21 |
+
|
22 |
+
|
23 |
+
def is_installed(lib_name: str) -> bool:
|
24 |
+
lib = shutil.which(lib_name)
|
25 |
+
if lib is None:
|
26 |
+
return False
|
27 |
+
global_path = Path(lib)
|
28 |
+
# else check if path is valid and has the correct access rights
|
29 |
+
return global_path.exists() and os.access(global_path, os.X_OK)
|
30 |
+
|
31 |
+
|
32 |
+
def play(audio: bytes, notebook: bool = False) -> None:
|
33 |
+
if notebook:
|
34 |
+
from IPython.display import Audio, display
|
35 |
+
|
36 |
+
display(Audio(audio, rate=44100, autoplay=True))
|
37 |
+
else:
|
38 |
+
if not is_installed("ffplay"):
|
39 |
+
raise ValueError("ffplay from ffmpeg not found, necessary to play audio.")
|
40 |
+
args = ["ffplay", "-autoexit", "-", "-nodisp"]
|
41 |
+
proc = subprocess.Popen(
|
42 |
+
args=args,
|
43 |
+
stdout=subprocess.PIPE,
|
44 |
+
stdin=subprocess.PIPE,
|
45 |
+
stderr=subprocess.PIPE,
|
46 |
+
)
|
47 |
+
_, err = proc.communicate(input=audio)
|
48 |
+
proc.poll()
|
49 |
+
|
50 |
+
|
51 |
+
def save(audio: bytes, filename: str) -> None:
|
52 |
+
with open(filename, "wb") as f:
|
53 |
+
f.write(audio)
|
54 |
+
|
55 |
+
|
56 |
+
def stream(audio_stream: Iterator[bytes]) -> bytes:
|
57 |
+
if not is_installed("mpv"):
|
58 |
+
raise ValueError("mpv not found, necessary to stream audio.")
|
59 |
+
|
60 |
+
mpv_command = ["mpv", "--no-cache", "--no-terminal", "--", "fd://0"]
|
61 |
+
mpv_process = subprocess.Popen(
|
62 |
+
mpv_command,
|
63 |
+
stdin=subprocess.PIPE,
|
64 |
+
stdout=subprocess.DEVNULL,
|
65 |
+
stderr=subprocess.DEVNULL,
|
66 |
+
)
|
67 |
+
|
68 |
+
audio = b""
|
69 |
+
|
70 |
+
for chunk in audio_stream:
|
71 |
+
if chunk is not None:
|
72 |
+
mpv_process.stdin.write(chunk) # type: ignore
|
73 |
+
mpv_process.stdin.flush() # type: ignore
|
74 |
+
audio += chunk
|
75 |
+
|
76 |
+
if mpv_process.stdin:
|
77 |
+
mpv_process.stdin.close()
|
78 |
+
mpv_process.wait()
|
79 |
+
|
80 |
+
return audio
|
utils/google_serper.py
ADDED
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Util that calls Google Search using the Serper.dev API."""
|
2 |
+
from typing import Any, Dict, List, Optional
|
3 |
+
|
4 |
+
import aiohttp
|
5 |
+
import requests
|
6 |
+
from typing_extensions import Literal
|
7 |
+
|
8 |
+
from pydantic import BaseModel
|
9 |
+
from utils.env import get_from_env
|
10 |
+
|
11 |
+
|
12 |
+
class GoogleSerper(BaseModel):
|
13 |
+
"""Wrapper around the Serper.dev Google Search API.
|
14 |
+
|
15 |
+
You can create a free API key at https://serper.dev.
|
16 |
+
|
17 |
+
To use, you should have the environment variable ``SERPER_API_KEY``
|
18 |
+
set with your API key, or pass `serper_api_key` as a named parameter
|
19 |
+
to the constructor.
|
20 |
+
"""
|
21 |
+
|
22 |
+
k: int = 10
|
23 |
+
gl: str = "us"
|
24 |
+
hl: str = "en"
|
25 |
+
# "places" and "images" is available from Serper but not implemented in the
|
26 |
+
# parser of run(). They can be used in results()
|
27 |
+
type: Literal["news", "search", "places", "images"] = "search"
|
28 |
+
result_key_for_type: dict = {
|
29 |
+
"news": "news",
|
30 |
+
"places": "places",
|
31 |
+
"images": "images",
|
32 |
+
"search": "organic",
|
33 |
+
}
|
34 |
+
|
35 |
+
tbs: Optional[str] = None
|
36 |
+
serper_api_key: Optional[str] = get_from_env("SERPER_API_KEY")
|
37 |
+
aiosession: Optional[aiohttp.ClientSession] = None
|
38 |
+
|
39 |
+
class Config:
|
40 |
+
"""Configuration for this pydantic object."""
|
41 |
+
|
42 |
+
arbitrary_types_allowed = True
|
43 |
+
|
44 |
+
def results(self, query: str, **kwargs: Any) -> Dict:
|
45 |
+
"""Run query through GoogleSearch."""
|
46 |
+
return self._google_serper_api_results(
|
47 |
+
query,
|
48 |
+
gl=self.gl,
|
49 |
+
hl=self.hl,
|
50 |
+
num=self.k,
|
51 |
+
tbs=self.tbs,
|
52 |
+
search_type=self.type,
|
53 |
+
**kwargs,
|
54 |
+
)
|
55 |
+
|
56 |
+
def run(self, query: str, **kwargs: Any) -> str:
|
57 |
+
"""Run query through GoogleSearch and parse result."""
|
58 |
+
results = self._google_serper_api_results(
|
59 |
+
query,
|
60 |
+
gl=self.gl,
|
61 |
+
hl=self.hl,
|
62 |
+
num=self.k,
|
63 |
+
tbs=self.tbs,
|
64 |
+
search_type=self.type,
|
65 |
+
**kwargs,
|
66 |
+
)
|
67 |
+
|
68 |
+
return self._parse_results(results)
|
69 |
+
|
70 |
+
async def aresults(self, query: str, **kwargs: Any) -> Dict:
|
71 |
+
"""Run query through GoogleSearch."""
|
72 |
+
results = await self._async_google_serper_search_results(
|
73 |
+
query,
|
74 |
+
gl=self.gl,
|
75 |
+
hl=self.hl,
|
76 |
+
num=self.k,
|
77 |
+
search_type=self.type,
|
78 |
+
tbs=self.tbs,
|
79 |
+
**kwargs,
|
80 |
+
)
|
81 |
+
return results
|
82 |
+
|
83 |
+
async def arun(self, query: str, **kwargs: Any) -> str:
|
84 |
+
"""Run query through GoogleSearch and parse result async."""
|
85 |
+
results = await self._async_google_serper_search_results(
|
86 |
+
query,
|
87 |
+
gl=self.gl,
|
88 |
+
hl=self.hl,
|
89 |
+
num=self.k,
|
90 |
+
search_type=self.type,
|
91 |
+
tbs=self.tbs,
|
92 |
+
**kwargs,
|
93 |
+
)
|
94 |
+
|
95 |
+
return self._parse_results(results)
|
96 |
+
|
97 |
+
def _parse_snippets(self, results: dict) -> List[str]:
|
98 |
+
snippets = []
|
99 |
+
|
100 |
+
if results.get("answerBox"):
|
101 |
+
answer_box = results.get("answerBox", {})
|
102 |
+
if answer_box.get("answer"):
|
103 |
+
return [answer_box.get("answer")]
|
104 |
+
elif answer_box.get("snippet"):
|
105 |
+
return [answer_box.get("snippet").replace("\n", " ")]
|
106 |
+
elif answer_box.get("snippetHighlighted"):
|
107 |
+
return answer_box.get("snippetHighlighted")
|
108 |
+
|
109 |
+
if results.get("knowledgeGraph"):
|
110 |
+
kg = results.get("knowledgeGraph", {})
|
111 |
+
title = kg.get("title")
|
112 |
+
entity_type = kg.get("type")
|
113 |
+
if entity_type:
|
114 |
+
snippets.append(f"{title}: {entity_type}.")
|
115 |
+
description = kg.get("description")
|
116 |
+
if description:
|
117 |
+
snippets.append(description)
|
118 |
+
for attribute, value in kg.get("attributes", {}).items():
|
119 |
+
snippets.append(f"{title} {attribute}: {value}.")
|
120 |
+
|
121 |
+
for result in results[self.result_key_for_type[self.type]][: self.k]:
|
122 |
+
if "snippet" in result:
|
123 |
+
snippets.append(result["snippet"])
|
124 |
+
for attribute, value in result.get("attributes", {}).items():
|
125 |
+
snippets.append(f"{attribute}: {value}.")
|
126 |
+
|
127 |
+
if len(snippets) == 0:
|
128 |
+
return ["No good Google Search Result was found"]
|
129 |
+
return snippets
|
130 |
+
|
131 |
+
def _parse_results(self, results: dict) -> str:
|
132 |
+
return " ".join(self._parse_snippets(results))
|
133 |
+
|
134 |
+
def _google_serper_api_results(
|
135 |
+
self, search_term: str, search_type: str = "search", **kwargs: Any
|
136 |
+
) -> dict:
|
137 |
+
headers = {
|
138 |
+
"X-API-KEY": self.serper_api_key or "",
|
139 |
+
"Content-Type": "application/json",
|
140 |
+
}
|
141 |
+
params = {
|
142 |
+
"q": search_term,
|
143 |
+
**{key: value for key, value in kwargs.items() if value is not None},
|
144 |
+
}
|
145 |
+
response = requests.post(
|
146 |
+
f"https://google.serper.dev/{search_type}", headers=headers, params=params
|
147 |
+
)
|
148 |
+
response.raise_for_status()
|
149 |
+
search_results = response.json()
|
150 |
+
return search_results
|
151 |
+
|
152 |
+
async def _async_google_serper_search_results(
|
153 |
+
self, search_term: str, search_type: str = "search", **kwargs: Any
|
154 |
+
) -> dict:
|
155 |
+
headers = {
|
156 |
+
"X-API-KEY": self.serper_api_key or "",
|
157 |
+
"Content-Type": "application/json",
|
158 |
+
}
|
159 |
+
url = f"https://google.serper.dev/{search_type}"
|
160 |
+
params = {
|
161 |
+
"q": search_term,
|
162 |
+
**{key: value for key, value in kwargs.items() if value is not None},
|
163 |
+
}
|
164 |
+
|
165 |
+
if not self.aiosession:
|
166 |
+
async with aiohttp.ClientSession() as session:
|
167 |
+
async with session.post(
|
168 |
+
url, params=params, headers=headers, raise_for_status=False
|
169 |
+
) as response:
|
170 |
+
search_results = await response.json()
|
171 |
+
else:
|
172 |
+
async with self.aiosession.post(
|
173 |
+
url, params=params, headers=headers, raise_for_status=True
|
174 |
+
) as response:
|
175 |
+
search_results = await response.json()
|
176 |
+
|
177 |
+
return search_results
|
178 |
+
|
179 |
+
|
180 |
+
if __name__ == "__main__":
|
181 |
+
serper = GoogleSerper()
|
182 |
+
serper.run("A langchain alternative")
|
utils/tts.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from os import getenv
|
2 |
+
|
3 |
+
import requests
|
4 |
+
|
5 |
+
from utils.functions import play, stream, save
|
6 |
+
|
7 |
+
ELEVEN_API_KEY = getenv("ELEVEN_API_KEY")
|
8 |
+
CHUNK_SIZE = 1024
|
9 |
+
ELEVENLABS_STREAM_ENDPOINT = "https://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream?optimize_streaming_latency=3"
|
10 |
+
ELEVENLABS_ENDPOINT = "https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
11 |
+
|
12 |
+
|
13 |
+
voices = {
|
14 |
+
"Bella": "EXAVITQu4vr4xnSDxMaL",
|
15 |
+
"Dorothy": "ThT5KcBeYPX3keUQqHPh",
|
16 |
+
"Male": "onwK4e9ZLuTAKqWW03F9",
|
17 |
+
"Chimamanda": "QSKN4kAq766BnZ0ilL0L",
|
18 |
+
"Ruth": "o9iLaGDMP3YCJcZevdfB",
|
19 |
+
"Ifeanyi": "iQe5hWADpVlprlflH1k8",
|
20 |
+
}
|
21 |
+
|
22 |
+
|
23 |
+
class TTS:
|
24 |
+
def __init__(self, voice_id):
|
25 |
+
self.voice_id = voice_id
|
26 |
+
self.headers = {
|
27 |
+
"Accept": "audio/mpeg",
|
28 |
+
"Content-Type": "application/json",
|
29 |
+
"xi-api-key": ELEVEN_API_KEY,
|
30 |
+
}
|
31 |
+
|
32 |
+
def generate(self, text, stream_: bool = False, model="eleven_monolingual_v1"):
|
33 |
+
data = {
|
34 |
+
"text": text,
|
35 |
+
"model_id": model,
|
36 |
+
"voice_settings": {"stability": 0.5, "similarity_boost": 0.0},
|
37 |
+
}
|
38 |
+
|
39 |
+
url = (
|
40 |
+
ELEVENLABS_STREAM_ENDPOINT.format(voice_id=self.voice_id)
|
41 |
+
if stream_
|
42 |
+
else ELEVENLABS_STREAM_ENDPOINT.format(voice_id=self.voice_id)
|
43 |
+
)
|
44 |
+
response = requests.post(
|
45 |
+
url,
|
46 |
+
json=data,
|
47 |
+
headers=self.headers,
|
48 |
+
stream=stream_,
|
49 |
+
)
|
50 |
+
|
51 |
+
if stream_:
|
52 |
+
audio_stream = (
|
53 |
+
chunk for chunk in response.iter_content(chunk_size=CHUNK_SIZE) if chunk
|
54 |
+
)
|
55 |
+
return audio_stream
|
56 |
+
else:
|
57 |
+
audio = response.content
|
58 |
+
return audio
|
utils/weather_forecast.py
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from typing import Optional
|
3 |
+
|
4 |
+
import httpx
|
5 |
+
from pydantic import BaseModel, Field
|
6 |
+
from simpleaichat import AIChat
|
7 |
+
|
8 |
+
from utils.env import get_from_env
|
9 |
+
|
10 |
+
WEATHERAPI_BASE_URL = "http://api.weatherapi.com/v1"
|
11 |
+
|
12 |
+
|
13 |
+
class WeatherAPI(BaseModel):
|
14 |
+
"""Wrapper for WeatherAPI
|
15 |
+
|
16 |
+
Docs for using:
|
17 |
+
|
18 |
+
1. Go to https://www.weatherapi.com/ and sign up for an API key
|
19 |
+
2. Save your API WEATHERAPI_API_KEY env variable
|
20 |
+
"""
|
21 |
+
|
22 |
+
weatherapi_key: Optional[str] = get_from_env("WEATHERAPI_API_KEY")
|
23 |
+
forecast_response: dict = None
|
24 |
+
warning: str = ""
|
25 |
+
|
26 |
+
def _format_weather_info(self):
|
27 |
+
current = self.forecast_response["current"]
|
28 |
+
forecast = self.forecast_response["forecast"]["forecastday"][0]
|
29 |
+
location_name = self.forecast_response["location"]["name"]
|
30 |
+
current_status = current["condition"]["text"]
|
31 |
+
wind_speed = current["wind_kph"]
|
32 |
+
wind_degree = current["wind_degree"]
|
33 |
+
humidity = current["humidity"]
|
34 |
+
cloud = current["cloud"]
|
35 |
+
feels_like = current["feelslike_c"]
|
36 |
+
current_temp = current["temp_c"]
|
37 |
+
min_temp = forecast["day"]["mintemp_c"]
|
38 |
+
max_temp = forecast["day"]["maxtemp_c"]
|
39 |
+
total_precip_mm = forecast["day"]["totalprecip_mm"]
|
40 |
+
total_snow_cm = forecast["day"]["totalsnow_cm"]
|
41 |
+
chance_of_rain = forecast["day"]["daily_chance_of_rain"]
|
42 |
+
chance_of_snow = forecast["day"]["daily_chance_of_snow"]
|
43 |
+
status = forecast["day"]["condition"]["text"]
|
44 |
+
|
45 |
+
is_day = current["is_day"]
|
46 |
+
sunrise = forecast["astro"]["sunrise"]
|
47 |
+
sunset = forecast["astro"]["sunset"]
|
48 |
+
# Convert the sunrise and sunset times to datetime objects
|
49 |
+
sunrise_time = datetime.strptime(sunrise, "%H:%M %p").time()
|
50 |
+
# sunset_time = datetime.strptime(sunset, "%I:%M %p").time()
|
51 |
+
|
52 |
+
# Get the current time
|
53 |
+
now = datetime.now().time()
|
54 |
+
|
55 |
+
# Check if the current time is before or after sunrise
|
56 |
+
if now < sunrise_time:
|
57 |
+
next_event = f"The sun will rise at {sunrise}."
|
58 |
+
else:
|
59 |
+
next_event = f"The sun will set at {sunset}."
|
60 |
+
rain_times = []
|
61 |
+
snow_times = []
|
62 |
+
for i, c in enumerate(forecast["hour"]):
|
63 |
+
if c["will_it_rain"]:
|
64 |
+
rain_times.append(c["time"])
|
65 |
+
if c["will_it_snow"]:
|
66 |
+
snow_times.append(c["time"])
|
67 |
+
|
68 |
+
results = (
|
69 |
+
f"In {location_name}, the current weather is as follows:\n"
|
70 |
+
f"Detailed status: {current_status}\n"
|
71 |
+
f"Wind speed: {wind_speed} kph, direction: {wind_degree}°\n"
|
72 |
+
f"Humidity: {humidity}%\n"
|
73 |
+
f"Temperature: \n"
|
74 |
+
f" - Current: {current_temp}°C\n"
|
75 |
+
f" - High: {max_temp}°C\n"
|
76 |
+
f" - Low: {min_temp}°C\n"
|
77 |
+
f" - Feels like: {feels_like}°C\n"
|
78 |
+
f"Cloud cover: {cloud}%\n"
|
79 |
+
f"Precipitation:\n"
|
80 |
+
f" - Total precipitation: {total_precip_mm} mm\n"
|
81 |
+
f" - Total snowfall: {total_snow_cm} cm\n"
|
82 |
+
f"Chance of precipitation:\n"
|
83 |
+
f" - Chance of rain: {chance_of_rain}%\n"
|
84 |
+
f" - Chance of snow: {chance_of_snow}%\n"
|
85 |
+
f"Weather status for the day: {status}\n"
|
86 |
+
+ (
|
87 |
+
f"It is currently daytime.\n"
|
88 |
+
if is_day
|
89 |
+
else f"It is currently nighttime.\n"
|
90 |
+
)
|
91 |
+
+ next_event
|
92 |
+
)
|
93 |
+
|
94 |
+
if rain_times:
|
95 |
+
results += "There is a chance of rain at the following times:\n"
|
96 |
+
for time in rain_times:
|
97 |
+
results += f"- {time}\n"
|
98 |
+
|
99 |
+
if snow_times:
|
100 |
+
results += "There is a chance of snow at the following times:\n"
|
101 |
+
for time in snow_times:
|
102 |
+
results += f"- {time}\n"
|
103 |
+
|
104 |
+
return f"{self.warning}\n{results}"
|
105 |
+
|
106 |
+
def get_forecast(self, location: dict):
|
107 |
+
"""Get the current weather information for a specified location."""
|
108 |
+
forecast_url = f"{WEATHERAPI_BASE_URL}/forecast.json"
|
109 |
+
forecast_params = {
|
110 |
+
"key": self.weatherapi_key,
|
111 |
+
"q": location["city"],
|
112 |
+
"format": "json",
|
113 |
+
"days": 1,
|
114 |
+
}
|
115 |
+
|
116 |
+
r = httpx.get(forecast_url, params=forecast_params).json()
|
117 |
+
self.forecast_response = r
|
118 |
+
|
119 |
+
def run(self, query) -> str | None:
|
120 |
+
location = get_location(query)
|
121 |
+
if not location:
|
122 |
+
self.warning = (
|
123 |
+
"Could not identify any location in the query. Defaulted to Halifax."
|
124 |
+
)
|
125 |
+
location = {"city": "Halifax", "country": "CA"}
|
126 |
+
self.get_forecast(location)
|
127 |
+
weather_info = self._format_weather_info()
|
128 |
+
return weather_info
|
129 |
+
|
130 |
+
|
131 |
+
class GetLocationMetadata(BaseModel):
|
132 |
+
"""Location information"""
|
133 |
+
|
134 |
+
city: str = Field(description="The city of the location.")
|
135 |
+
state: int = Field(
|
136 |
+
description="The state or province of the location. Must be the full state name"
|
137 |
+
)
|
138 |
+
state_code: int = Field(
|
139 |
+
description="The state or province of the location. Must be a 2-char string."
|
140 |
+
)
|
141 |
+
country: str = Field(
|
142 |
+
description="The country of the location. Country must be a 2-char string"
|
143 |
+
)
|
144 |
+
|
145 |
+
|
146 |
+
def get_location(query: str) -> dict | None:
|
147 |
+
params = {"temperature": 0.0, "max_tokens": 100}
|
148 |
+
system_prompt = (
|
149 |
+
"You reply ONLY with the location information. If no location is detected, respond with None in "
|
150 |
+
"each field"
|
151 |
+
)
|
152 |
+
ai = AIChat(system=system_prompt, params=params)
|
153 |
+
# noinspection PyTypeChecker
|
154 |
+
location: dict = ai(query, output_schema=GetLocationMetadata)
|
155 |
+
if location["city"] == "None":
|
156 |
+
return None
|
157 |
+
return location
|