megamined commited on
Commit
b36a86c
1 Parent(s): b69d0ff

Initial commit

Browse files
Files changed (10) hide show
  1. .gitignore +160 -0
  2. app.py +114 -0
  3. environment.yml +15 -0
  4. main.py +74 -0
  5. requirements.txt +6 -0
  6. utils/env.py +30 -0
  7. utils/functions.py +80 -0
  8. utils/google_serper.py +182 -0
  9. utils/tts.py +58 -0
  10. 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