├── .env.example
├── .github
└── workflows
│ ├── publish.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── agentcomms
├── __init__.py
├── adminpanel
│ ├── __init__.py
│ ├── constants.py
│ ├── files.py
│ ├── page.py
│ ├── server.py
│ └── tests
│ │ ├── __init__.py
│ │ ├── files.py
│ │ └── server.py
├── discord
│ ├── __init__.py
│ ├── actions.py
│ ├── connector.py
│ └── tests
│ │ ├── __init__.py
│ │ └── tests.py
└── twitter
│ ├── __init__.py
│ ├── actions.py
│ ├── connector.py
│ └── tests
│ ├── __init__.py
│ └── tests.py
├── requirements.txt
├── resources
├── image.jpg
└── youcreatethefuture.jpg
├── setup.py
└── test.py
/.env.example:
--------------------------------------------------------------------------------
1 | OPENAI_API_KEY=
2 |
3 | TWITTER_EMAIL=
4 | TWITTER_USERNAME=
5 | TWITTER_PASSWORD=
6 |
7 | DISCORD_API_TOKEN=
8 |
9 | ELEVENLABS_API_KEY=
10 | ELEVENLABS_VOICE="Rachel"
11 | ELEVENLABS_MODEL="eleven_monolingual_v1"
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.x'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: ${{ secrets.pypi_username }}
39 | password: ${{ secrets.pypi_password }}
40 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Lint and Test
2 |
3 | on: [push]
4 |
5 | env:
6 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
7 | TWITTER_EMAIL: ${{ secrets.TWITTER_EMAIL }}
8 | TWITTER_USERNAME: ${{ secrets.TWITTER_USERNAME }}
9 | TWITTER_PASSWORD: ${{ secrets.TWITTER_PASSWORD }}
10 | DISCORD_API_TOKEN: ${{ secrets.DISCORD_API_TOKEN }}
11 | ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
12 | ELEVENLABS_VOICE: ${{ secrets.ELEVENLABS_VOICE }}
13 | ELEVENLABS_MODEL: ${{ secrets.ELEVENLABS_MODEL }}
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | python-version: ["3.10"]
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Set up Python ${{ matrix.python-version }}
24 | uses: actions/setup-python@v3
25 | with:
26 | python-version: ${{ matrix.python-version }}
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install pytest
31 | pip install -r requirements.txt
32 | - name: Running tests
33 | run: |
34 | pytest test.py
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | .DS_Store
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | .pybuilder/
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | # For a library or package, you might want to ignore these files since the code is
88 | # intended to run in multiple environments; otherwise, check them in:
89 | # .python-version
90 |
91 | # pipenv
92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
95 | # install all needed dependencies.
96 | #Pipfile.lock
97 |
98 | # poetry
99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100 | # This is especially recommended for binary packages to ensure reproducibility, and is more
101 | # commonly ignored for libraries.
102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103 | #poetry.lock
104 |
105 | # pdm
106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107 | #pdm.lock
108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109 | # in version control.
110 | # https://pdm.fming.dev/#use-with-ide
111 | .pdm.toml
112 |
113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114 | __pypackages__/
115 |
116 | # Celery stuff
117 | celerybeat-schedule
118 | celerybeat.pid
119 |
120 | # SageMath parsed files
121 | *.sage.py
122 |
123 | # Environments
124 | .env
125 | .venv
126 | env/
127 | venv/
128 | ENV/
129 | env.bak/
130 | venv.bak/
131 |
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 |
136 | # Rope project settings
137 | .ropeproject
138 |
139 | # mkdocs documentation
140 | /site
141 |
142 | # mypy
143 | .mypy_cache/
144 | .dmypy.json
145 | dmypy.json
146 |
147 | # Pyre type checker
148 | .pyre/
149 |
150 | # pytype static type analyzer
151 | .pytype/
152 |
153 | # Cython debug symbols
154 | cython_debug/
155 |
156 | # PyCharm
157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159 | # and can be added to the global gitignore or merged into this file. For a more nuclear
160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161 | #.idea/
162 |
163 | .vscode/
164 | .chroma
165 | memory
166 | test
167 | files/
168 | .env
169 | twitter.cookies
170 | temp/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 M̵̞̗̝̼̅̏̎͝Ȯ̴̝̻̊̃̋̀Õ̷̼͋N̸̩̿͜ ̶̜̠̹̼̩͒
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # agentcomms
2 |
3 | Connectors for your agent to the outside world.
4 |
5 | - Discord connector with (voice and chat, DMs coming)
6 | - Twitter connector (feed only, DMs coming)
7 | - Admin Panel - simple web interface to chat with your agent and upload files
8 |
9 |
10 |
11 | [](https://github.com/AutonomousResearchGroup/agentcomms/actions/workflows/test.yml)
12 | [](https://badge.fury.io/py/agentcomms)
13 |
14 | # Installation
15 |
16 | ```bash
17 | pip install agentcomms
18 | ```
19 |
20 | # Twitter Usage Guide
21 |
22 | This module uses a set of environment variables to interact with Twitter, so you'll need to set the following before using:
23 |
24 | - `TWITTER_EMAIL`: The email address for your Twitter account.
25 | - `TWITTER_USERNAME`: The username for your Twitter account.
26 | - `TWITTER_PASSWORD`: The password for your Twitter account.
27 |
28 | ## Setting up the Twitter connector
29 |
30 | Before you can start using the Twitter connector, you have to initialize it. The initialization is done using the `start_twitter_connector` function.
31 |
32 | ```python
33 | import twitter
34 | twitter.start_twitter_connector()
35 | ```
36 |
37 | This will start the twitter connector with default parameters. If you wish to customize the email, username, password, or session storage path, you can use the `start_twitter` function like so:
38 |
39 | ```python
40 | twitter.start_twitter(email="my_email@example.com", username="my_username", password="my_password", session_storage_path="my_session.cookies")
41 | ```
42 |
43 | ### Liking a tweet
44 |
45 | To like a tweet, you can use the `like_tweet` function. Pass in the id of the tweet you wish to like.
46 |
47 | ```python
48 | twitter.like_tweet("1234567890")
49 | ```
50 |
51 | ### Replying to a tweet
52 |
53 | To reply to a tweet, you can use the `reply_to_tweet` function. Pass in the message you wish to send, and the id of the tweet you're replying to.
54 |
55 | ```python
56 | twitter.reply_to_tweet("This is a great tweet!", "1234567890")
57 | ```
58 |
59 | ### Posting a tweet
60 |
61 | To post a new tweet, you can use the `tweet` function. Pass in the message you wish to tweet. You can optionally pass in a media object to attach to the tweet.
62 |
63 | ```python
64 | twitter.tweet("Hello, Twitter!")
65 | ```
66 |
67 | ## Registering feed handlers
68 |
69 | Feed handlers are functions that get called whenever there are new tweets in the feed. They can be registered using the `register_feed_handler` function.
70 |
71 | ```python
72 | def my_feed_handler(tweet):
73 | print(f"New tweet from {tweet['user']['name']}: {tweet['text']}")
74 |
75 | twitter.register_feed_handler(my_feed_handler)
76 | ```
77 |
78 | You can also unregister a handler using the `unregister_feed_handler` function.
79 |
80 | ```python
81 | twitter.unregister_feed_handler(my_feed_handler)
82 | ```
83 |
84 | ## Getting account information
85 |
86 | To get the current account object, you can use the `get_account` function.
87 |
88 | ```python
89 | account = twitter.get_account()
90 | print(account.username)
91 | ```
92 |
93 | This will print out the username of the current Twitter account.
94 |
95 | # Discord Usage Guide
96 |
97 | The Discord connector works with both voice and text. For voice, you will need an Elevenlabs API key.
98 |
99 | ## Environment Variables
100 |
101 | Before you start, you need to set the environment variables for the bot to function correctly. Create a `.env` file in your project directory and set these variables:
102 |
103 | ```env
104 | DISCORD_API_TOKEN=your_discord_bot_token
105 | ELEVENLABS_API_KEY=your_elevenlabs_api_key
106 | ELEVENLABS_VOICE=voice_you_want_to_use
107 | ELEVENLABS_MODEL=model_you_want_to_use
108 | ```
109 |
110 | - `DISCORD_API_TOKEN` is your Discord bot token, which you get when you create a new bot on the Discord developer portal.
111 | - `ELEVENLABS_API_KEY` is your Eleven Labs API key for their TTS service.
112 | - `ELEVENLABS_VOICE` is the voice you want to use for the TTS. You will have to check the Eleven Labs API documentation for the voices they support.
113 | - `ELEVENLABS_MODEL` is the TTS model you want to use. Again, you will have to check the Eleven Labs API documentation for the supported models.
114 |
115 | ## Running the Bot
116 |
117 | After setting your environment variables, you can run your bot by calling the `start_connector` function:
118 |
119 | ```python
120 | start_connector()
121 | ```
122 |
123 | ## Registering Message Handlers
124 |
125 | Message handlers are functions that are executed when certain events happen in Discord, such as receiving a message. Here's how you can register a message handler:
126 |
127 | ### Create the Handler Function
128 |
129 | First, you need to create a function that will be executed when a message is received. This function should take one argument, which is the message that was received. The message object will contain all the information about the message, such as the content of the message, the author, and the channel where it was sent.
130 |
131 | Here's an example of a simple message handler function:
132 |
133 | ```python
134 | def handle_message(message):
135 | print(f"Received a message from {message.author}: {message.content}")
136 | ```
137 |
138 | This function will simply print the author and content of every message that is received.
139 |
140 | ### Register the Handler Function
141 |
142 | To register the handler function, you use the `register_feed_handler` function and pass the handler function as an argument:
143 |
144 | ```python
145 | register_feed_handler(handle_message)
146 | ```
147 |
148 | After calling this function, the `handle_message` function will be executed every time a message is received on Discord.
149 |
150 | ## Public Functions
151 |
152 | ### `send_message(message: str, channel_id: int)`
153 |
154 | This function is used to add a message to the queue. The message will be sent to the channel with the ID specified.
155 |
156 | ```python
157 | send_message("Hello world!", 1234567890)
158 | ```
159 |
160 | ### `start_connector(discord_api_token: str)`
161 |
162 | This function is used to start the bot and the event loop, setting the bot to listen for events on Discord.
163 |
164 | ```python
165 | start_connector("your_discord_api_token")
166 | ```
167 |
168 | ### `register_feed_handler(func: callable)`
169 |
170 | This function is used to register a new function as a feed handler. Feed handlers are functions that process or respond to incoming data in some way.
171 |
172 | ```python
173 | def my_func(data):
174 | print(data)
175 |
176 | register_feed_handler(my_func)
177 | ```
178 |
179 | ### `unregister_feed_handler(func: callable)`
180 |
181 | This function is used to remove a function from the list of feed handlers.
182 |
183 | ```python
184 | unregister_feed_handler(my_func)
185 | ```
186 |
187 | # Admin Panel Usage Guide
188 |
189 | ## Quickstart
190 |
191 | 1. **Start the server**:
192 | You can start the server with uvicorn like this:
193 |
194 | ```python
195 | import os
196 |
197 | if __name__ == "__main__":
198 | import uvicorn
199 | uvicorn.run("agentcomms:start_server", host="0.0.0.0", port=int(os.getenv("PORT", 8000)))
200 | ```
201 |
202 | This will start the server at `http://localhost:8000`.
203 |
204 | 2. **Get a file**:
205 | Once the server is up and running, you can retrieve file content by sending a GET request to `/file/{path}` endpoint, where `{path}` is the path to the file relative to the server's current storage directory.
206 |
207 | ```python
208 | from agentcomms import get_file
209 |
210 | # Fetches the content of the file located at "./files/test.txt"
211 | file_content = get_file("test.txt")
212 | print(file_content)
213 | ```
214 |
215 | 3. **Save a file**:
216 | Similarly, you can save content to a file by sending a POST request to `/file/` endpoint, with JSON data containing the `path` and `content` parameters.
217 |
218 | ```python
219 | from agentcomms import add_file
220 |
221 | # Creates a file named "test.txt" in the current storage directory
222 | # and writes "Hello, world!" to it.
223 | add_file("test.txt", "Hello, world!")
224 | ```
225 |
226 | ## API Documentation
227 |
228 | AgentFS provides the following public functions:
229 |
230 | ### `start_server(storage_path=None)`
231 |
232 | Starts the FastAPI server. If a `storage_path` is provided, it sets the storage directory to the given path.
233 |
234 | **Arguments**:
235 |
236 | - `storage_path` (str, optional): The path to the storage directory.
237 |
238 | **Returns**:
239 |
240 | - None
241 |
242 | **Example**:
243 |
244 | ```python
245 | from agentcomms import start_server
246 |
247 | start_server("/my/storage/directory")
248 | ```
249 |
250 | ### `get_server()`
251 |
252 | Returns the FastAPI application instance.
253 |
254 | **Arguments**:
255 |
256 | - None
257 |
258 | **Returns**:
259 |
260 | - FastAPI application instance.
261 |
262 | **Example**:
263 |
264 | ```python
265 | from agentcomms import get_server
266 |
267 | app = get_server()
268 | ```
269 |
270 | ### `set_storage_path(new_path)`
271 |
272 | Sets the storage directory to the provided path.
273 |
274 | **Arguments**:
275 |
276 | - `new_path` (str): The path to the new storage directory.
277 |
278 | **Returns**:
279 |
280 | - `True` if the path was successfully set, `False` otherwise.
281 |
282 | **Example**:
283 |
284 | ```python
285 | from agentcomms import set_storage_path
286 |
287 | set_storage_path("/my/storage/directory")
288 | ```
289 |
290 | ### `add_file(path, content)`
291 |
292 | Creates a file at the specified path and writes the provided content to it.
293 |
294 | **Arguments**:
295 |
296 | - `path` (str): The path to the new file.
297 | - `content` (str): The content to be written to the file.
298 |
299 | **Returns**:
300 |
301 | - `True` if the file was successfully created.
302 |
303 | **Example**:
304 |
305 | ```python
306 | from agentcomms import add_file
307 |
308 | add_file("test.txt", "Hello, world!")
309 | ```
310 |
311 | ### `remove_file(path)`
312 |
313 | Removes the file at the specified path.
314 |
315 | **Arguments**:
316 |
317 | - `path` (str): The path to the file to be removed.
318 |
319 | **Returns**:
320 |
321 | - `True` if the file was successfully removed.
322 |
323 | **Example**:
324 |
325 | ```python
326 | from agentcomms import remove_file
327 |
328 | remove_file("test.txt")
329 | ```
330 |
331 | ### `update_file(path, content)`
332 |
333 | Appends the provided content to the file at the specified path.
334 |
335 | **Arguments**:
336 |
337 | - `path` (str): The path to the file to be updated.
338 | - `content` (str): The content to be appended to the file.
339 |
340 | **Returns**:
341 |
342 | - `True` if the file was successfully updated.
343 |
344 | **Example**:
345 |
346 | ```python
347 | from agentcomms import update_file
348 |
349 | update_file("test.txt", "New content")
350 | ```
351 |
352 | ### `list_files(path='.')`
353 |
354 | Lists all files in the specified directory.
355 |
356 | **Arguments**:
357 |
358 | - `path` (str, optional): The path to the directory. Defaults to `'.'` (current directory).
359 |
360 | **Returns**:
361 |
362 | - A list of file names in the specified directory.
363 |
364 | **Example**:
365 |
366 | ```python
367 | from agentcomms import list_files
368 |
369 | files = list_files()
370 | ```
371 |
372 | ### `list_files_formatted(path='.')`
373 |
374 | Lists all files in the specified directory as a formatted string. Convenient!
375 |
376 | **Arguments**:
377 |
378 | - `path` (str, optional): The path to the directory. Defaults to `'.'` (current directory).
379 |
380 | **Returns**:
381 |
382 | - A string containing a list of file names in the specified directory.
383 |
384 | **Example**:
385 |
386 | ```python
387 | from agentcomms import list_files
388 |
389 | files = list_files()
390 | ```
391 |
392 | ### `get_file(path)`
393 |
394 | Returns the content of the file at the specified path.
395 |
396 | **Arguments**:
397 |
398 | - `path` (str): The path to the file.
399 |
400 | **Returns**:
401 |
402 | - A string containing the content of the file.
403 |
404 | **Example**:
405 |
406 | ```python
407 | from agentcomms import get_file
408 |
409 | content = get_file("test.txt")
410 | ```
411 |
412 | # Contributions Welcome
413 |
414 | If you like this library and want to contribute in any way, please feel free to submit a PR and I will review it. Please note that the goal here is simplicity and accesibility, using common language and few dependencies.
415 |
--------------------------------------------------------------------------------
/agentcomms/__init__.py:
--------------------------------------------------------------------------------
1 | from .discord import *
2 | from .twitter import *
--------------------------------------------------------------------------------
/agentcomms/adminpanel/__init__.py:
--------------------------------------------------------------------------------
1 | from .files import (
2 | get_storage_path,
3 | set_storage_path,
4 | add_file,
5 | remove_file,
6 | update_file,
7 | list_files,
8 | list_files_formatted,
9 | get_file,
10 | )
11 | from .server import (
12 | start_server,
13 | get_server,
14 | set_storage_path,
15 | send_message,
16 | async_send_message,
17 | register_message_handler,
18 | unregister_message_handler,
19 | )
20 |
21 | __all__ = [
22 | "get_storage_path",
23 | "start_server",
24 | "send_message",
25 | "async_send_message",
26 | "register_message_handler",
27 | "unregister_message_handler",
28 | "get_server",
29 | "set_storage_path",
30 | "add_file",
31 | "remove_file",
32 | "update_file",
33 | "list_files",
34 | "list_files_formatted",
35 | "get_file",
36 | ]
37 |
--------------------------------------------------------------------------------
/agentcomms/adminpanel/constants.py:
--------------------------------------------------------------------------------
1 | app = None
2 |
3 | storage_path = "./files/"
4 |
--------------------------------------------------------------------------------
/agentcomms/adminpanel/files.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from agentcomms.adminpanel.constants import storage_path
4 |
5 |
6 | def check_files():
7 | if not os.path.exists(storage_path):
8 | os.makedirs(storage_path, exist_ok=True)
9 |
10 | def get_storage_path():
11 | return storage_path
12 |
13 | def set_storage_path(new_path):
14 | global storage_path
15 | if os.path.exists(new_path): # Check if the new_path exists
16 | storage_path = new_path
17 | return True
18 | else:
19 | return False # The new path doesn't exist
20 |
21 |
22 | def add_file(path, content):
23 | check_files()
24 | with open(os.path.join(storage_path, path), "w") as f:
25 | f.write(content)
26 | return True
27 |
28 |
29 | def remove_file(path):
30 | check_files()
31 | os.remove(os.path.join(storage_path, path)) # Removes the file
32 | return True
33 |
34 |
35 | def update_file(path, content):
36 | check_files()
37 | with open(os.path.join(storage_path, path), "a") as f: # 'a' for appending
38 | f.write(content)
39 | return True
40 |
41 |
42 | def list_files(path="."):
43 | check_files()
44 | return os.listdir(os.path.join(storage_path, path)) # Returns the list of files
45 |
46 | def list_files_formatted(path="."):
47 | check_files()
48 | files = os.listdir(os.path.join(storage_path, path))
49 | files_formatted = []
50 | for file in files:
51 | if os.path.isdir(os.path.join(storage_path, path, file)):
52 | files_formatted.append(os.path.join(storage_path, file + "/"))
53 | else:
54 | files_formatted.append(os.path.join(storage_path, file))
55 | return "My Files:\n" + "\n".join(files_formatted)
56 |
57 | def get_file(path):
58 | check_files()
59 | with open(os.path.join(storage_path, path), "r") as f: # 'r' for reading
60 | content = f.read()
61 | return content
62 |
--------------------------------------------------------------------------------
/agentcomms/adminpanel/page.py:
--------------------------------------------------------------------------------
1 | page = """\
2 |
3 |
4 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
130 |
236 |
237 |
238 | """
239 |
--------------------------------------------------------------------------------
/agentcomms/adminpanel/server.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import asyncio
4 | from fastapi import APIRouter, FastAPI, File, Form, HTTPException, UploadFile
5 | from fastapi.staticfiles import StaticFiles
6 | from pydantic import BaseModel
7 |
8 | from agentcomms.adminpanel.constants import app, storage_path
9 | from agentcomms.adminpanel.files import check_files, get_storage_path, set_storage_path
10 | from fastapi import FastAPI, WebSocket, WebSocketDisconnect
11 | from fastapi.middleware.cors import CORSMiddleware
12 | from fastapi.staticfiles import StaticFiles
13 | from fastapi.responses import FileResponse, HTMLResponse
14 |
15 | from agentcomms.adminpanel.page import page
16 | from concurrent.futures import ThreadPoolExecutor
17 |
18 | executor = ThreadPoolExecutor(max_workers=1)
19 |
20 | router = APIRouter()
21 | app = FastAPI()
22 |
23 | ws: WebSocket = None
24 |
25 | handlers = []
26 | loop = None
27 |
28 |
29 | class FilePath(BaseModel):
30 | path: str
31 |
32 |
33 | def get_server():
34 | """Retrieve the global FastAPI instance."""
35 | global app
36 | return app
37 |
38 |
39 | def get_parent_path():
40 | """Return the absolute path of the parent directory of this script."""
41 | return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42 |
43 |
44 | def start_server(storage_path=None, port=8000):
45 | """
46 | Start the FastAPI server.
47 |
48 | :param storage_path: The path where to store the files.
49 | :param port: The port on which to start the server.
50 | :return: The FastAPI application.
51 | """
52 | # start an event loop
53 | global loop
54 | loop = asyncio.new_event_loop()
55 | global app
56 | if storage_path:
57 | set_storage_path(storage_path)
58 | check_files()
59 | app.include_router(router)
60 | app.add_middleware(
61 | CORSMiddleware,
62 | allow_origins=["*"],
63 | allow_credentials=True,
64 | allow_methods=["*"],
65 | allow_headers=["*"],
66 | )
67 |
68 | app.mount(
69 | "/files", StaticFiles(directory=get_storage_path(), html=False), name="files"
70 | )
71 | if port:
72 | os.environ["PORT"] = str(port)
73 |
74 | return app
75 |
76 |
77 | def stop_server():
78 | """Stop the FastAPI server by setting the global app to None."""
79 | global app
80 | app = None
81 |
82 |
83 | @app.get("/")
84 | async def get():
85 | """Handle a GET request to the root of the server, responding with an HTML page."""
86 | return HTMLResponse(page)
87 |
88 |
89 | def send_message(message, type="chat", source="default"):
90 | """
91 | Send a message to the websocket.
92 |
93 | :param message: The message to send.
94 | """
95 | global ws
96 | global loop
97 | if ws is not None and loop is not None:
98 | message = json.dumps({"type": type, "message": message, "source": source})
99 | asyncio.run(ws.send_text(message))
100 |
101 |
102 | async def async_send_message(message, type="chat", source="default"):
103 | """
104 | Send a message to the websocket.
105 |
106 | :param message: The message to send.
107 | """
108 | global ws
109 | global loop
110 | if ws is not None and loop is not None:
111 | message = json.dumps({"type": type, "message": message, "source": source})
112 | await ws.send_text(message)
113 |
114 | def register_message_handler(handler):
115 | """
116 | Register a handler for messages received through the websocket.
117 |
118 | :param handler: The handler to register.
119 | """
120 | global handlers
121 | handlers.append(handler)
122 |
123 |
124 | def unregister_message_handler(handler):
125 | """
126 | Unregister a handler for messages received through the websocket.
127 |
128 | :param handler: The handler to unregister.
129 | """
130 | global handlers
131 | handlers.remove(handler)
132 |
133 |
134 | @app.websocket("/ws")
135 | async def websocket_endpoint(websocket: WebSocket):
136 | """
137 | Establish a websocket connection.
138 |
139 | :param websocket: The websocket through which to communicate.
140 | """
141 | global ws
142 | ws = websocket
143 | await websocket.accept()
144 | try:
145 | while True:
146 | data = await websocket.receive_text()
147 | # data is a string, convert to json
148 | data = json.loads(data)
149 | for handler in handlers:
150 | await handler(data)
151 | except WebSocketDisconnect:
152 | ws = None
153 |
154 |
155 | @router.post("/file/")
156 | async def http_add_file(path: str = Form(...), file: UploadFile = File(...)):
157 | """
158 | Create a new file at a given path with the provided content.
159 |
160 | :param path: The path where to create the file.
161 | :param file: The content to put in the file.
162 | :return: A success message.
163 | """
164 | check_files()
165 | with open(os.path.join(storage_path, path), "wb") as f:
166 | f.write(await file.read())
167 | return {"message": "File created"}
168 |
169 |
170 | @router.delete("/file/{path}")
171 | def http_remove_file(path: str):
172 | """
173 | Delete a file at a given path.
174 |
175 | :param path: The path of the file to delete.
176 | :return: A success message.
177 | """
178 | check_files()
179 | try:
180 | os.remove(os.path.join(storage_path, path))
181 | return {"message": "File removed"}
182 | except Exception as e:
183 | raise HTTPException(status_code=400, detail=str(e))
184 |
185 |
186 | @router.put("/file/")
187 | async def http_update_file(path: str = Form(...), file: UploadFile = File(...)):
188 | """
189 | Update a file at a given path with the provided content.
190 |
191 | :param path: The path of the file to update.
192 | :param file: The new content to put in the file.
193 | :return: A success message.
194 | """
195 | check_files()
196 | with open(os.path.join(storage_path, path), "wb") as f:
197 | f.write(await file.read())
198 | return {"message": "File updated"}
199 |
200 |
201 | @router.get("/files/")
202 | def http_list_files(path: str = "."):
203 | """
204 | List all files in a given directory.
205 |
206 | :param path: The path of the directory.
207 | :return: A list of files.
208 | """
209 | check_files()
210 | return {"files": os.listdir(os.path.join(storage_path, path))}
211 |
212 |
213 | @router.get("/file/{path}")
214 | def http_get_file(path: str):
215 | """
216 | Retrieve a file at a given path.
217 |
218 | :param path: The path of the file to retrieve.
219 | :return: The file as a response.
220 | """
221 | check_files()
222 | try:
223 | return FileResponse(os.path.join(storage_path, path))
224 | except Exception as e:
225 | raise HTTPException(status_code=400, detail=str(e))
--------------------------------------------------------------------------------
/agentcomms/adminpanel/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from .files import *
2 | from .server import *
--------------------------------------------------------------------------------
/agentcomms/adminpanel/tests/files.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from agentcomms.adminpanel.files import (
4 | add_file,
5 | get_file,
6 | list_files,
7 | list_files_formatted,
8 | remove_file,
9 | set_storage_path,
10 | update_file,
11 | )
12 |
13 | # Define a test directory
14 | TEST_DIR = "./test_dir/"
15 |
16 |
17 | def setup_module():
18 | os.makedirs(TEST_DIR, exist_ok=True)
19 | set_storage_path(TEST_DIR)
20 |
21 |
22 | def teardown_module():
23 | # check if TEST_DIR exists
24 | if not os.path.exists(TEST_DIR):
25 | return
26 | for file in os.listdir(TEST_DIR):
27 | os.remove(os.path.join(TEST_DIR, file))
28 | os.rmdir(TEST_DIR)
29 |
30 |
31 | def test_add_file():
32 | setup_module()
33 | assert add_file("test.txt", "Hello, world!")
34 | assert os.path.isfile(os.path.join(TEST_DIR, "test.txt"))
35 | teardown_module()
36 |
37 |
38 | def test_get_file():
39 | setup_module()
40 | add_file("test.txt", "Hello, world!")
41 | assert get_file("test.txt") == "Hello, world!"
42 | teardown_module()
43 |
44 |
45 | def test_update_file():
46 | setup_module()
47 | assert update_file("test.txt", "Hello, world! Updated")
48 | assert get_file("test.txt") == "Hello, world! Updated"
49 | teardown_module()
50 |
51 |
52 | def test_list_files():
53 | setup_module()
54 | add_file("test.txt", "Hello, world!")
55 | assert "test.txt" in list_files()
56 | teardown_module()
57 |
58 |
59 | def test_list_files_formatted():
60 | setup_module()
61 | add_file("test.txt", "Hello, world!")
62 | assert "test.txt" in list_files_formatted()
63 | teardown_module()
64 |
65 |
66 | def test_remove_file():
67 | setup_module()
68 | add_file("test.txt", "Hello, world!")
69 | assert remove_file("test.txt")
70 | assert "test.txt" not in list_files()
71 | teardown_module()
72 |
--------------------------------------------------------------------------------
/agentcomms/adminpanel/tests/server.py:
--------------------------------------------------------------------------------
1 | from fastapi.testclient import TestClient
2 | from agentcomms.adminpanel.server import start_server
3 |
4 | client = TestClient(start_server())
5 |
6 |
7 | def test_http_add_file():
8 | file_contents = "Hello, world!"
9 | response = client.post(
10 | "/file/",
11 | data={"path": "test.txt"},
12 | files={"file": ("test.txt", file_contents, "text/plain")},
13 | )
14 | assert response.status_code == 200
15 | assert response.json() == {"message": "File created"}
16 |
17 |
18 | def test_http_get_file():
19 | response = client.get("/file/test.txt")
20 | assert response.status_code == 200
21 | assert response.content.decode() == "Hello, world!"
22 |
23 |
24 | def test_http_update_file():
25 | file_contents = " Updated"
26 | response = client.put(
27 | "/file/",
28 | data={"path": "test.txt"},
29 | files={"file": ("test.txt", file_contents, "text/plain")},
30 | )
31 | assert response.status_code == 200
32 | assert response.json() == {"message": "File updated"}
33 |
34 |
35 | def test_http_list_files():
36 | test_http_add_file()
37 | response = client.get("/files/")
38 | assert response.status_code == 200
39 | assert "test.txt" in response.json()["files"]
40 |
41 |
42 | def test_http_remove_file():
43 | response = client.delete("/file/test.txt")
44 | assert response.status_code == 200
45 | assert response.json() == {"message": "File removed"}
46 |
--------------------------------------------------------------------------------
/agentcomms/discord/__init__.py:
--------------------------------------------------------------------------------
1 | from .connector import (
2 | start_connector as start_discord_connector,
3 | send_message,
4 | register_feed_handler,
5 | unregister_feed_handler,
6 | )
7 |
8 | __all__ = [
9 | "start_discord_connector",
10 | "send_message",
11 | "register_feed_handler",
12 | "unregister_feed_handler",
13 | ]
14 |
--------------------------------------------------------------------------------
/agentcomms/discord/actions.py:
--------------------------------------------------------------------------------
1 | # actions for send_message, send_dm, join_channel and leave_channel
--------------------------------------------------------------------------------
/agentcomms/discord/connector.py:
--------------------------------------------------------------------------------
1 | import discord as discord_py
2 | import os
3 | import openai
4 | import time
5 | import asyncio
6 | from random import *
7 | from elevenlabs import set_api_key, generate, save
8 | from dotenv import load_dotenv
9 | from threading import Thread
10 |
11 | load_dotenv()
12 |
13 | message_queue = asyncio.Queue()
14 |
15 | temp_folder_tts = "./temp"
16 | temp_path = "./temp/output.mp3"
17 |
18 | intents = discord_py.Intents().all()
19 | intents.message_content = True
20 | bot = discord_py.Client(intents=intents)
21 | vc = None
22 |
23 |
24 | async def send_queued_messages():
25 | """
26 | Function to send all messages that have been queued.
27 | """
28 | while True:
29 | print("Waiting for message")
30 | message, channel_id = await message_queue.get()
31 | channel = bot.get_channel(channel_id)
32 | if channel is not None:
33 | # Send the message
34 | # determine if this is a voice channel
35 | if channel.type == discord_py.ChannelType.voice:
36 | try:
37 | sources = []
38 | sentences = message.split("\n")
39 | for sentence in sentences:
40 | tts_reply = generate_tts(str(sentence))
41 | sources.append(tts_reply)
42 | for source in sources:
43 | vc.play(discord_py.FFmpegPCMAudio(source))
44 | while vc.is_playing():
45 | await asyncio.sleep(0.10)
46 | except:
47 | raise
48 | else:
49 | await channel.send(message)
50 |
51 | # Mark the message as handled
52 | message_queue.task_done()
53 | print("Message sent from queue")
54 |
55 |
56 | def send_message(message, channel_id):
57 | """
58 | Function to send a message. Adds the message to the queue.
59 |
60 | Args:
61 | message (str): Message to send
62 | channel_id (int): ID of the channel where the message will be sent
63 | """
64 | message_queue.put_nowait((message, channel_id))
65 | print("Message sent")
66 |
67 |
68 | def start_connector(loop_dict=None, discord_api_token=None):
69 | """
70 | Starts the Discord bot connector
71 |
72 | Args:
73 | discord_api_token (str, optional): Discord API token. Defaults to None.
74 | """
75 | global bot
76 | print("Starting Discord connector")
77 | if discord_api_token is None:
78 | discord_api_token = os.getenv("DISCORD_API_TOKEN")
79 | print("Discord connector started")
80 |
81 | def bot_thread():
82 | loop = asyncio.new_event_loop()
83 | asyncio.set_event_loop(loop)
84 |
85 | async def main():
86 | await bot.start(discord_api_token)
87 |
88 | loop.run_until_complete(asyncio.gather(main(), send_queued_messages()))
89 |
90 | # Create a new thread that will run the bot
91 | t = Thread(target=bot_thread, daemon=True)
92 | t.start()
93 | return t
94 |
95 |
96 | def generate_tts(message):
97 | """
98 | Generates TTS for a given message.
99 |
100 | Args:
101 | message (str): Message to convert to speech.
102 |
103 | Returns:
104 | str: File path of the audio file.
105 | """
106 | set_api_key(os.getenv("ELEVENLABS_API_KEY"))
107 | voice = os.getenv("ELEVENLABS_VOICE")
108 | model = os.getenv("ELEVENLABS_MODEL")
109 | timestamp = str(time.time())
110 | # check that temp_folder_tts exists
111 | if not os.path.exists(temp_folder_tts):
112 | os.makedirs(temp_folder_tts)
113 | file_path = temp_folder_tts + "/reply_" + timestamp + ".mp3"
114 | print(message)
115 | audio = generate(text=message, voice=voice, model=model, stream=False)
116 | save(audio, file_path)
117 | return file_path
118 |
119 |
120 | handlers = []
121 |
122 |
123 | def register_feed_handler(func):
124 | """
125 | Registers a new feed handler.
126 |
127 | Args:
128 | func (callable): Function to be registered as a handler.
129 | """
130 | handlers.append(func)
131 |
132 |
133 | def unregister_feed_handler(func):
134 | """
135 | Unregisters a feed handler.
136 |
137 | Args:
138 | func (callable): Function to be unregistered as a handler.
139 | """
140 | handlers.remove(func)
141 |
142 |
143 | @bot.event
144 | async def on_ready():
145 | """
146 | Callback function that is called when the bot is ready.
147 | """
148 | print(f"{bot.user} has connected to Discord!")
149 |
150 |
151 | @bot.event
152 | async def on_message(message):
153 | """
154 | Callback function that is called when a new message is received.
155 |
156 | Args:
157 | message (Message): Message object.
158 | """
159 | global vc
160 | if message.author == bot.user:
161 | return
162 |
163 | if not message.guild.me.permissions_in(message.channel).manage_messages:
164 | print("Missing permissions to manage messages")
165 | return
166 |
167 | if discord_py.utils.get(bot.voice_clients, guild=message.guild) != None:
168 | if (
169 | discord_py.utils.get(bot.voice_clients, guild=message.guild).is_playing()
170 | == True
171 | ):
172 | await message.delete()
173 | return
174 |
175 | # Execute registered handlers
176 | for handler in handlers:
177 | await handler(message)
178 |
179 | contents = message.content
180 | speaker_id = contents
181 | await message.delete()
182 | channel = message.channel
183 | members = channel.members
184 |
185 | for themember in members:
186 | if themember.id == int(speaker_id):
187 | voice = themember.voice
188 |
189 | voice_client = discord_py.utils.get(bot.voice_clients, guild=voice.channel.guild)
190 | if voice_client is None:
191 | vc = await voice.channel.connect()
192 | else:
193 | vc = voice_client
194 |
195 | if os.path.exists(temp_path):
196 | os.remove(temp_path)
197 |
198 | # Assuming you have a start_recording method implemented in your voice client
199 | vc.start_recording(
200 | discord_py.sinks.MP3Sink(),
201 | vgpt_after,
202 | voice.channel,
203 | )
204 |
205 | await asyncio.sleep(5)
206 | vc.stop_recording()
207 |
208 | while not os.path.exists(temp_path):
209 | await asyncio.sleep(0.1)
210 | audio_file = open(temp_path, "rb")
211 | openai.api_key = os.getenv("OPENAI_API_KEY")
212 | transcript = openai.Audio.transcribe("whisper-1", audio_file)
213 | transcript = str(transcript.text)
214 |
215 | if transcript == "ok":
216 | return
217 |
218 | message["speaker_id"] = speaker_id
219 | message["transcript"] = transcript
220 | message["vc"] = vc
221 |
222 | for handler in handlers:
223 | # if handler is async, await it
224 | if asyncio.iscoroutinefunction(handler):
225 | await handler(message)
226 | else:
227 | handler(message)
228 | return message
229 |
230 |
231 | async def vgpt_after(sink: discord_py.sinks, channel: discord_py.TextChannel, *args):
232 | """
233 | Post-processing function to be executed after receiving a message.
234 |
235 | Args:
236 | sink (discord_py.sinks): Audio sink.
237 | channel (discord_py.TextChannel): Channel where the message was received.
238 | args: Additional arguments.
239 | """
240 | user_id = ""
241 | for user_id, audio in sink.audio_data.items():
242 | user_id = f"<@{user_id}>"
243 | with open(temp_path, "wb") as f:
244 | f.write(audio.file.getbuffer())
245 |
--------------------------------------------------------------------------------
/agentcomms/discord/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from .tests import *
--------------------------------------------------------------------------------
/agentcomms/discord/tests/tests.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | from agentcomms.discord import start_discord_connector
4 | from dotenv import load_dotenv
5 | from elevenlabs import set_api_key
6 |
7 | from agentcomms.discord.connector import send_message
8 |
9 | load_dotenv()
10 | voice = os.getenv("ELEVENLABS_VOICE")
11 | model = os.getenv("ELEVENLABS_MODEL")
12 | set_api_key(os.getenv("ELEVENLABS_API_KEY"))
13 |
14 | # def test_generate_tts():
15 | # message = "Hello, world!"
16 |
17 | # file_path = generate_tts(message)
18 |
19 | # # Check that the file path is constructed correctly
20 | # assert file_path.startswith("./temp/reply_")
21 | # assert file_path.endswith(".mp3")
22 |
23 | # # Check that the file has been created
24 | # assert os.path.exists(file_path)
25 |
26 | # print("TTS file generated successfully")
27 |
28 | # # Additional assertions could check the contents of the file or other properties
29 |
30 |
31 | def test_start_discord_connector(capfd):
32 | # Call start_connector function
33 | thread = start_discord_connector()
34 | time.sleep(2)
35 | send_message("Hello, world!", 1107883421759447040)
36 | time.sleep(2)
37 | # Check the print output
38 | captured = capfd.readouterr()
39 | assert "Starting Discord connector" in captured.out
40 | assert "Discord connector started" in captured.out
41 | print(captured.out)
42 |
43 | # Stop the bot if you need to
44 | # loop = asyncio.get_event_loop()
45 | # loop.run_until_complete(bot.close())
46 |
--------------------------------------------------------------------------------
/agentcomms/twitter/__init__.py:
--------------------------------------------------------------------------------
1 | from .connector import (
2 | start_connector as start_twitter_connector,
3 | start_twitter,
4 | like_tweet,
5 | reply_to_tweet,
6 | tweet,
7 | search_tweets,
8 | get_authors,
9 | get_relevant_tweets_from_author_timeline,
10 | register_feed_handler,
11 | unregister_feed_handler,
12 | get_account
13 | )
14 |
15 | __all__ = [
16 | "start_twitter_connector",
17 | "register_feed_handler",
18 | "unregister_feed_handler",
19 | "start_twitter",
20 | "like_tweet",
21 | "reply_to_tweet",
22 | "tweet",
23 | "get_account",
24 | "search_tweets",
25 | "get_authors"
26 | ]
27 |
--------------------------------------------------------------------------------
/agentcomms/twitter/actions.py:
--------------------------------------------------------------------------------
1 | # actions for send_message, reply_to and like
--------------------------------------------------------------------------------
/agentcomms/twitter/connector.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pdb
3 | import asyncio
4 | from pathlib import Path
5 | import random
6 | import threading
7 | import time
8 | from httpx import Client
9 | from twitter.account import Account
10 | from twitter.scraper import Scraper
11 | from twitter.search import Search
12 | import orjson
13 |
14 | from twitter.util import init_session
15 |
16 | from dotenv import load_dotenv
17 |
18 | load_dotenv()
19 |
20 | first = True
21 |
22 | params = {
23 | "include_profile_interstitial_type": 1,
24 | "include_blocking": 1,
25 | "include_blocked_by": 1,
26 | "include_followed_by": 1,
27 | "include_want_retweets": 1,
28 | "include_mute_edge": 1,
29 | "include_can_dm": 1,
30 | "include_can_media_tag": 1,
31 | "include_ext_has_nft_avatar": 1,
32 | "include_ext_is_blue_verified": 1,
33 | "include_ext_verified_type": 1,
34 | "include_ext_profile_image_shape": 1,
35 | "skip_status": 1,
36 | "cards_platform": "Web-12",
37 | "include_cards": 1,
38 | "include_ext_alt_text": "true",
39 | "include_ext_limited_action_results": "true",
40 | "include_quote_count": "true",
41 | "include_reply_count": 1,
42 | "tweet_mode": "extended",
43 | "include_ext_views": "true",
44 | "include_entities": "true",
45 | "include_user_entities": "true",
46 | "include_ext_media_color": "true",
47 | "include_ext_media_availability": "true",
48 | "include_ext_sensitive_media_warning": "true",
49 | "include_ext_trusted_friends_metadata": "true",
50 | "send_error_codes": "true",
51 | "simple_quoted_tweet": "true",
52 | "count": 20,
53 | "requestContext": "launch",
54 | "ext": "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,superFollowMetadata,unmentionInfo,editControl",
55 | }
56 |
57 | feed_handlers = []
58 | dm_handlers = []
59 |
60 | account = None
61 | search = None
62 |
63 | def get_account():
64 | """
65 | Returns the global account object.
66 | """
67 | return account
68 |
69 |
70 | def like_tweet(tweet_id):
71 | """
72 | Likes a tweet with the given tweet ID using the global account object.
73 |
74 | Parameters:
75 | tweet_id (str): The ID of the tweet to be liked.
76 | """
77 | account.like(tweet_id)
78 |
79 |
80 | def reply_to_tweet(message, tweet_id):
81 | """
82 | Replies to a tweet with the given message and tweet ID using the global account object.
83 |
84 | Parameters:
85 | message (str): The message to be sent as a reply.
86 | tweet_id (str): The ID of the tweet to reply to.
87 | """
88 | account.reply(message, tweet_id)
89 |
90 |
91 | def tweet(message, media=None):
92 | """
93 | Posts a new tweet with the given message and optional media using the global account object.
94 |
95 | Parameters:
96 | message (str): The message to be tweeted.
97 | media (str, optional): The media to be attached to the tweet. Defaults to None.
98 | """
99 | if media:
100 | account.tweet(message, media)
101 | else:
102 | account.tweet(message)
103 |
104 |
105 | def fetch_data(query, limit, retries):
106 | """
107 | Fetches data based on the provided search query.
108 |
109 | Args:
110 | - query (str): The search query.
111 | - limit (int): Maximum number of results to return.
112 | - retries (int): Number of times to retry the search in case of failure.
113 |
114 | Returns:
115 | - list[dict]: List of search results.
116 | """
117 | queries = [
118 | {
119 | 'category': 'Latest',
120 | 'query': query
121 | },
122 | {
123 | 'category': 'Top',
124 | 'query': query
125 | }
126 | ]
127 | results = search.run(limit=limit, retries=retries, queries=queries)
128 | results = [data for result in results for data in result]
129 | return results
130 |
131 | def search_tweets(topic, min_results=30, max_retries=5, **filters):
132 | """
133 | Searches for tweets based on provided criteria and returns a filtered list.
134 |
135 | Parameters:
136 | topic (str): The primary search term or phrase.
137 | min_results (int, optional): Minimum desired number of results. Default is 30.
138 | max_retries (int, optional): Maximum number of retries in case of search failures. Default is 5.
139 | filters (dict, optional): Additional parameters to filter the search results.
140 |
141 | Returns:
142 | list: A list of tweets that match the search criteria.
143 |
144 | TODO:
145 | - Enhance query structure:
146 | - Support advanced query operations (e.g., AND, OR).
147 | - Implement exact phrase matching and exclusion of terms.
148 | - Integrate time-based filters for refined search.
149 | - Incorporate Twitter Spaces transcriptions for deeper content insights.
150 | - Develop logic for handling retweets and associated content nuances.
151 | """
152 |
153 | # Extracting and constructing search filters
154 | min_faves = filters.get('min_faves', 0)
155 | min_retweets = filters.get('min_retweets', 0)
156 | min_replies = filters.get('min_replies', 0)
157 | verified_only = filters.get('verified_only', False)
158 |
159 | # TODO direct mapping for now.
160 | # TODO Generate queries from topic similar to above.
161 | query = topic
162 | # Appending filters to the base query
163 | if min_faves:
164 | query += f" min_faves:{min_faves}"
165 | if min_retweets:
166 | query += f" min_retweets:{min_retweets}"
167 | if min_replies:
168 | query += f" min_replies:{min_replies}"
169 | if verified_only:
170 | query += " filter:verified"
171 |
172 | results = fetch_data(query, min_results, max_retries)
173 | # concatenation hack
174 | return results
175 |
176 |
177 | def get_authors(tweets_data, **filters):
178 | """
179 | Extracts a list of authors from the provided tweet data based on specified filters.
180 |
181 | Parameters:
182 | tweets_data (list[dict]): A list of dictionaries representing tweet data.
183 | filters (dict): Optional filters to refine the list of authors, including 'follower_count_min', 'search_keywords', and 'verified_only'.
184 |
185 | Returns:
186 | dict: A dictionary mapping screen names to author data.
187 | """
188 |
189 | def calculate_impact_factor(tweet_impact_data):
190 | """Compute the impact factor based on various tweet metrics."""
191 | return sum([
192 | tweet_impact_data.get(metric, 0)
193 | for metric in ['bookmark_count', 'favorite_count', 'reply_count', 'retweet_count']
194 | ])
195 |
196 | # Extract filters
197 | follower_count_min = filters.get('follower_count_min', 0)
198 | verified_only = filters.get('verified_only', False)
199 |
200 | authors = {}
201 |
202 | for tweet_data in tweets_data:
203 | # Extract relevant data points
204 | pdb.set_trace()
205 | tweet_result = tweet_data.get('content', {}).get('itemContent', {}).get('tweet_results', {}).get('result', {})
206 | user_data_core = tweet_result.get('core', {}).get('user_results', {}).get('result', {})
207 | user_data = user_data_core.get('legacy', {})
208 | tweet_impact_data = tweet_result.get('legacy', {})
209 | tweet_impact_data['views'] = tweet_result.get('views', {})
210 |
211 | # Checks
212 | if "tweet" not in tweet_data.get('entryId', '') or not user_data or not tweet_impact_data:
213 | continue
214 | if verified_only and not user_data_core.get('is_blue_verified', False):
215 | continue
216 | if user_data.get('followers_count', 0) < follower_count_min:
217 | continue
218 |
219 | # Calculate impact factor
220 | impact_factor = calculate_impact_factor(tweet_impact_data)
221 |
222 | # Extract and accumulate author data
223 | author_info = {
224 | 'id': user_data.get('id'),
225 | 'name': user_data.get('name'),
226 | 'screen_name': user_data.get('screen_name'),
227 | 'followers_count': user_data.get('followers_count'),
228 | 'description': user_data.get('description'),
229 | 'favourites_count': user_data.get('favourites_count'),
230 | 'friends_count': user_data.get('friends_count'),
231 | 'normal_follower_count': user_data.get('normal_followers_count'),
232 | 'views': int(tweet_impact_data.get('views', {}).get('count', 0)),
233 | 'impact_factor': impact_factor,
234 | }
235 | author_url = author_info['screen_name']
236 | if author_url not in authors:
237 | authors[author_url] = author_info
238 | else:
239 | # pdb.set_trace()
240 | authors[author_url]['views'] += int(author_info['views'])
241 | authors[author_url]['impact_factor'] += author_info['impact_factor']
242 |
243 | # TODO Further enhancements: Normalize impact factor, consider tweet age/time, etc.
244 | return authors
245 |
246 |
247 | """
248 | CHATGPT PROMPT:
249 | I want to search tweets to rank authors who write about a particular topic, you are my `research and media assistant`. Given a topic, give me relevant queries I should search for on twitter. The queries should be ranked based on usage.
250 |
251 | topic:
252 | AI Existential Risk
253 | queries:
254 |
255 | REPLY:
256 | For the topic "AI Existential Risk," here's a list of potential queries ranked based on usage and relevance:
257 |
258 | 1. "AI Existential Risk"
259 | 2. "Artificial Intelligence existential threat"
260 | 3. "AI and global catastrophic risks"
261 | 4. "Dangers of superintelligent AI"
262 | 5. "Risks of advanced AI"
263 | 6. "Long-term AI safety concerns"
264 | 7. "Unintended consequences of AI"
265 | 8. "AI and end of humanity concerns"
266 | 9. "Machine learning existential threats"
267 | 10. "Ethical concerns of powerful AI"
268 |
269 | These queries encapsulate various ways people might discuss the existential risks of AI on Twitter. They range from the direct ("AI Existential Risk") to the more nuanced or indirect ("Ethical concerns of powerful AI").
270 | """
271 | # TODO Generate queries from topic similar to above.
272 |
273 | def extract_document_from_tweet(tweet_data):
274 | """
275 | Extracts relevant information from a tweet to construct a document.
276 |
277 | Args:
278 | - tweet_data (dict): The raw tweet data.
279 |
280 | Returns:
281 | - str: A formatted document containing relevant information from the tweet.
282 | """
283 | tweet_result = tweet_data.get('content', {}).get('itemContent', {}).get('tweet_results', {}).get('result', {})
284 | user_data = tweet_result.get('core', {}).get('user_results', {}).get('result', {}).get('legacy', {})
285 |
286 | return (f"author_name: {user_data.get('name')}\n"
287 | f"screen_name: {user_data.get('screen_name')}\n"
288 | f"full_text: {tweet_result.get('legacy', {}).get('full_text', '')}\n"
289 | f"created_at: {tweet_result.get('legacy', {}).get('created_at', '')}\n\n\n")
290 |
291 |
292 | def get_relevant_tweets_from_author_timeline(topic, author, min_results=30, max_retries=5, **filters):
293 | """
294 | Fetches and processes tweets related to a given topic from a specific author's timeline.
295 |
296 | Args:
297 | - topic (str): The main topic or keyword for which tweets are to be fetched.
298 | - author (str): The screen name of the Twitter user.
299 | - min_results (int): Minimum number of results to return per query.
300 | - max_retries (int): Number of times to retry the search in case of failure.
301 |
302 | Returns:
303 | - dict: A dictionary where the key is the conversation_id and the value is a document constructed from all relevant tweets.
304 | """
305 | query = topic + f" (from:{author})"
306 | tweets_data = fetch_data(query, min_results, max_retries)
307 |
308 | documents = {}
309 |
310 | for tweet_data in tweets_data:
311 | pdb.set_trace()
312 | tweet_result = tweet_data.get('content', {}).get('itemContent', {}).get('tweet_results', {}).get('result', {})
313 | conversation_id = tweet_result['legacy']['conversation_id_str']
314 |
315 | if conversation_id in documents:
316 | continue
317 | pdb.set_trace()
318 | quote_tweet_id = int(tweet_result.get('legacy', {}).get('quoted_status_id_str', 0))
319 | conversation_data = fetch_data(f"(conversation_id:{conversation_id})", min_results, max_retries)
320 | try:
321 | origin_tweet_data = [scraper.tweets_details([int(conversation_id)])[0]['data']['threaded_conversation_with_injections_v2']['instructions'][0]['entries'][0]]
322 | # TODO handle case where origin_tweet has quote_tweet
323 | # use origin_tweet_data to update quote_tweet_id from 0 to x.
324 | quote_tweet_data = [scraper.tweets_details([quote_tweet_id])[0]['data']['threaded_conversation_with_injections_v2']['instructions'][0]['entries'][0]] if quote_tweet_id else []
325 | except Exception as e:
326 | print(f"An error occurred: {e}")
327 | print("Error: Error scraping for origin_tweet_data and quote_tweet_data")
328 | origin_tweet_data = []
329 | quote_tweet_data = []
330 | all_tweets_in_conversation = origin_tweet_data + quote_tweet_data + conversation_data
331 | documents[conversation_id] = ''.join([extract_document_from_tweet(data) for data in all_tweets_in_conversation])
332 |
333 | return documents
334 |
335 | def register_feed_handler(handler):
336 | """
337 | Registers a new feed handler function. The handler function will be called whenever new feed messages are received.
338 |
339 | Parameters:
340 | handler (function): The function to be registered as a feed handler.
341 | """
342 | feed_handlers.append(handler)
343 |
344 |
345 | def unregister_feed_handler(handler):
346 | """
347 | Unregisters a feed handler function. The handler will no longer be called when new feed messages are received.
348 |
349 | Parameters:
350 | handler (function): The function to be unregistered.
351 | """
352 | feed_handlers.remove(handler)
353 |
354 |
355 | # async def dm_loop(account, session, scraper):
356 | # """
357 | # Main DM loop. This function checks for new DMs and passes them to all registered feed handlers.
358 |
359 | # Parameters:
360 | # account (twitter.account.Account): The account object to be used for the DM operations.
361 | # session (httpx.Client): The httpx session to be used for the DM operations.
362 | # scraper (twitter.scraper.Scraper): The Scraper object to be used for the DM operations.
363 | # """
364 | # last_responded_notification = None
365 | # global params
366 |
367 | # while True:
368 | # inbox = account.dm_inbox()
369 | # for handler in feed_handlers:
370 | # arguments = {
371 | # "inbox": inbox,
372 | # "account": account,
373 | # "session": session,
374 | # }
375 |
376 | # # if the handler is async, await it, otherwise call directly
377 | # if asyncio.iscoroutinefunction(handler):
378 | # await handler(arguments)
379 | # else:
380 | # handler(arguments)
381 |
382 | # await asyncio.sleep(10 + random.randint(0, 2))
383 |
384 |
385 | def feed_loop(account, session):
386 | """
387 | Main feed loop. This function checks for new tweets and passes them to all registered feed handlers.
388 |
389 | Parameters:
390 | account (twitter.account.Account): The account object to be used for the feed operations.
391 | session (httpx.Client): The httpx session to be used for the feed operations.
392 | """
393 | last_responded_notification = None
394 | global params
395 | global first
396 | cursor = None
397 |
398 | while True:
399 | if first == True:
400 | params["count"] = 10
401 | del params["requestContext"]
402 | first = False
403 | time.sleep(7)
404 | continue
405 | # Parse the JSON response
406 | notifications = account.notifications()
407 | globalObjects = notifications.get("globalObjects", {})
408 | object_notifications = globalObjects.get("notifications", {})
409 | instructions = notifications["timeline"]["instructions"]
410 | new_entries = []
411 | for x in instructions:
412 | addEntries = x.get("addEntries")
413 | if addEntries:
414 | entries = addEntries["entries"]
415 | new_entries.append(entries)
416 | for x in entries:
417 | entryId = x["entryId"]
418 | find_cursor_top = entryId.find("cursor-top")
419 | if find_cursor_top != -1:
420 | content = x["content"]
421 | operation = content.get("operation")
422 | if operation:
423 | cursor = operation["cursor"]["value"]
424 | if cursor:
425 | params["cursor"] = cursor
426 |
427 | # Extract all tweet notifications from the response
428 | tweet_notifications = [
429 | notification
430 | for notification in object_notifications.values()
431 | if "text" in notification["message"]
432 | ]
433 |
434 | # Sort notifications by timestamp (newest first)
435 | tweet_notifications.sort(key=lambda x: x["timestampMs"], reverse=True)
436 | tweet_id = None
437 | for notification in tweet_notifications:
438 | # if print(notification["message"]["text"]) includes "There was a login", continue
439 | if (
440 | "There was a login" in notification["message"]["text"]
441 | or "liked a Tweet you" in notification["message"]["text"]
442 | ):
443 | continue
444 |
445 | # Skip notifications we've already responded to
446 | if (
447 | last_responded_notification is not None
448 | and notification["timestampMs"] <= last_responded_notification
449 | ):
450 | continue
451 |
452 | targetObjects = (
453 | notification.get("template", {})
454 | .get("aggregateUserActionsV1", {})
455 | .get("targetObjects", [{}])
456 | )
457 | if len(targetObjects) > 0:
458 | tweet_id = targetObjects[0].get("tweet", {}).get("id")
459 |
460 | tweet_details = scraper.tweets_details([tweet_id])
461 |
462 | # get the tweet
463 |
464 | arguments = {
465 | "tweet_id": tweet_id,
466 | "notification": notification,
467 | "tweet": tweet_details[0],
468 | "account": account,
469 | "session": session,
470 | }
471 |
472 | # call response handlers here with the notification as the argument
473 | for handler in feed_handlers:
474 | # if the handler is async, await it, otherwise call directly
475 | if asyncio.iscoroutinefunction(handler):
476 | asyncio.run(handler(arguments))
477 | else:
478 | handler(arguments)
479 |
480 | # Update the latest responded notification timestamp
481 | last_responded_notification = notification["timestampMs"]
482 |
483 | time.sleep(10 + random.randint(0, 2))
484 |
485 |
486 | def start_twitter(
487 | email=None,
488 | username=None,
489 | password=None,
490 | session_storage_path="twitter.cookies",
491 | start_loop=True,
492 | loop_dict=None,
493 | ):
494 | """
495 | Starts the Twitter connector. Initializes the global account object and starts the feed loop in a new thread.
496 |
497 | Parameters:
498 | email (str, optional): The email address to be used for the Twitter account. If not provided, will be loaded from environment variables.
499 | username (str, optional): The username to be used for the Twitter account. If not provided, will be loaded from environment variables.
500 | password (str, optional): The password to be used for the Twitter account. If not provided, will be loaded from environment variables.
501 | session_storage_path (str, optional): The path where session data should be stored. Defaults to "twitter.cookies".
502 | start_loop (bool, optional): Whether the feed loop should be started immediately. Defaults to True.
503 |
504 | Returns:
505 | threading.Thread: The thread where the feed loop is running.
506 | """
507 | global account
508 | global search
509 | global scraper
510 | # get the environment variables
511 | if email is None:
512 | email = os.getenv("TWITTER_EMAIL")
513 | if username is None:
514 | username = os.getenv("TWITTER_USERNAME")
515 | if password is None:
516 | password = os.getenv("TWITTER_PASSWORD")
517 |
518 | session_file = Path(session_storage_path)
519 | session = None
520 |
521 | if session_file.exists():
522 | cookies = orjson.loads(session_file.read_bytes())
523 | session = Client(cookies=cookies)
524 | account = Account(session=session)
525 | scraper = Scraper(session=session)
526 | else:
527 | session = init_session()
528 | account = Account(email=email, username=username, password=password)
529 | scraper = Scraper(session=session)
530 | cookies = {
531 | k: v
532 | for k, v in account.session.cookies.items()
533 | if k in {"ct0", "auth_token"}
534 | }
535 | session_file.write_bytes(orjson.dumps(cookies))
536 |
537 | search = Search(email=email, username=username, password=password, save=True, debug=1)
538 |
539 | if start_loop:
540 | thread = threading.Thread(target=feed_loop, args=(account, session, scraper))
541 | thread.start()
542 | return thread
543 |
544 |
545 | def start_connector(
546 | loop_dict=None, # dict of information for stopping the loop, etc
547 | email=None,
548 | username=None,
549 | password=None,
550 | session_storage_path="twitter.cookies",
551 | start_loop=True,
552 | ):
553 | """
554 | Convenience function to start the Twitter connector. This function calls start_twitter with the default arguments.
555 | """
556 | start_twitter(
557 | email=None,
558 | username=None,
559 | password=None,
560 | session_storage_path="twitter.cookies",
561 | start_loop=True,
562 | loop_dict=None,
563 | )
564 |
--------------------------------------------------------------------------------
/agentcomms/twitter/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from .tests import *
--------------------------------------------------------------------------------
/agentcomms/twitter/tests/tests.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | import json
4 | import pdb
5 | from agentcomms.twitter import (
6 | start_twitter,
7 | like_tweet,
8 | reply_to_tweet,
9 | tweet,
10 | search_tweets,
11 | get_authors,
12 | get_relevant_tweets_from_author_timeline,
13 | register_feed_handler,
14 | unregister_feed_handler,
15 | )
16 |
17 | # Define constants or load them from environment variables
18 | TWITTER_EMAIL = os.getenv("TWITTER_EMAIL")
19 | TWITTER_USERNAME = os.getenv("TWITTER_USERNAME")
20 | TWITTER_PASSWORD = os.getenv("TWITTER_PASSWORD")
21 | SESSION_STORAGE_PATH = "twitter.cookies"
22 |
23 |
24 |
25 | # Custom setup function
26 | def setup_function():
27 | # Call the start_connector function and check behavior (this is likely to have side effects)
28 | start_twitter(
29 | email=TWITTER_EMAIL,
30 | username=TWITTER_USERNAME,
31 | password=TWITTER_PASSWORD,
32 | session_storage_path=SESSION_STORAGE_PATH,
33 | start_loop=False,
34 | )
35 |
36 |
37 | # Add more tests as required for other functions and behaviors
38 | def test_like_tweet():
39 | setup_function()
40 | tweet_id = "1687657490584928256" # Replace with actual tweet ID
41 | like_tweet(tweet_id)
42 |
43 |
44 | def test_reply_to_tweet():
45 | setup_function()
46 | message = "Test reply!"
47 | tweet_id = "1687657490584928256" # Replace with actual tweet ID
48 | reply_to_tweet(message, tweet_id)
49 |
50 |
51 | def test_tweet():
52 | setup_function()
53 | message = "Test tweet!"
54 | tweet(message)
55 |
56 |
57 | def test_search_tweets(topic="Upstreet", limit=100, **kwargs):
58 | setup_function()
59 | res = search_tweets(topic, limit, retries=5, **kwargs)
60 | return res
61 |
62 |
63 | def test_get_authors(tweets_data=None, **kwargs):
64 | setup_function()
65 | if tweets_data is None:
66 | tweets_data = test_search_tweets()
67 | pdb.set_trace()
68 | authors = get_authors(tweets_data)
69 | authors = dict(sorted(authors.items(), key=lambda x: x[1]['impact_factor'], reverse=True))
70 | return authors
71 |
72 |
73 | def test_get_relevant_tweets_from_author_timeline(topic=None, author=None):
74 | setup_function()
75 | if topic is None and author is None:
76 | topic = "Attention"
77 | author = "GenZSiv"
78 | documents = get_relevant_tweets_from_author_timeline(topic, author)
79 | return documents
80 |
81 |
82 | def test_register_and_unregister_feed_handler():
83 | setup_function()
84 | handler = lambda x: print(x)
85 | register_feed_handler(handler)
86 | # Here you can add logic to verify the handler was registered, such as checking the feed_handlers list
87 | unregister_feed_handler(handler)
88 |
89 |
90 | if __name__=='__main__':
91 |
92 | topic = "Attention"
93 | author = "GenZSiv"
94 |
95 | tweets_data = test_search_tweets(topic , 5)
96 | authors = test_get_authors(tweets_data)
97 |
98 | with open(f'{topic}_data.json', 'w') as file:
99 | json.dump(tweets_data, file, indent=4)
100 |
101 | with open(f'{topic}_authors.json', 'w') as file:
102 | json.dump(authors, file, indent=4)
103 |
104 | documents = test_get_relevant_tweets_from_author_timeline(topic, author)
105 |
106 | with open(f'documents_{author}_{topic}.json', 'w') as file:
107 | json.dump(documents, file, indent=4)
108 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | openai
3 | pydantic
4 | python-multipart
5 | python-dotenv
6 | httpx
7 | py-cord[voice]
8 | twitter-api-client
9 | elevenlabs
--------------------------------------------------------------------------------
/resources/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elizaOS/agentcomms/b037497170cc3b7cab6e20ffac842864ab8a4e61/resources/image.jpg
--------------------------------------------------------------------------------
/resources/youcreatethefuture.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elizaOS/agentcomms/b037497170cc3b7cab6e20ffac842864ab8a4e61/resources/youcreatethefuture.jpg
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | # read the contexts of requirements.txt into an array
4 | required = [
5 | "fastapi",
6 | "openai",
7 | "pydantic",
8 | "python-multipart",
9 | "python-dotenv",
10 | "httpx",
11 | "py-cord[voice]",
12 | "twitter-api-client",
13 | "elevenlabs",
14 | ]
15 |
16 | long_description = ""
17 | with open("README.md", "r") as fh:
18 | long_description = fh.read()
19 | # search for any lines that contain