├── .gitignore
├── .vscode
├── launch.json
└── settings.json
├── CONTRIBUTING.md
├── INSTALL_AND_USAGE.md
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── philoagents-api
├── .dockerignore
├── .env.example
├── .python-version
├── Dockerfile
├── Makefile
├── README.md
├── data
│ ├── evaluation_dataset.json
│ └── extraction_metadata.json
├── langgraph.json
├── notebooks
│ ├── long_term_memory_in_action.ipynb
│ └── short_term_memory_in_action.ipynb
├── pyproject.toml
├── src
│ └── philoagents
│ │ ├── __init__.py
│ │ ├── application
│ │ ├── __init__.py
│ │ ├── conversation_service
│ │ │ ├── __init__.py
│ │ │ ├── generate_response.py
│ │ │ ├── reset_conversation.py
│ │ │ └── workflow
│ │ │ │ ├── __init__.py
│ │ │ │ ├── chains.py
│ │ │ │ ├── edges.py
│ │ │ │ ├── graph.py
│ │ │ │ ├── nodes.py
│ │ │ │ ├── state.py
│ │ │ │ └── tools.py
│ │ ├── data
│ │ │ ├── __init__.py
│ │ │ ├── deduplicate_documents.py
│ │ │ └── extract.py
│ │ ├── evaluation
│ │ │ ├── __init__.py
│ │ │ ├── evaluate.py
│ │ │ ├── generate_dataset.py
│ │ │ └── upload_dataset.py
│ │ ├── long_term_memory.py
│ │ └── rag
│ │ │ ├── __init__.py
│ │ │ ├── embeddings.py
│ │ │ ├── retrievers.py
│ │ │ └── splitters.py
│ │ ├── config.py
│ │ ├── domain
│ │ ├── __init__.py
│ │ ├── evaluation.py
│ │ ├── exceptions.py
│ │ ├── philosopher.py
│ │ ├── philosopher_factory.py
│ │ └── prompts.py
│ │ └── infrastructure
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── mongo
│ │ ├── __init__.py
│ │ ├── client.py
│ │ └── indexes.py
│ │ └── opik_utils.py
├── tools
│ ├── call_agent.py
│ ├── create_long_term_memory.py
│ ├── delete_long_term_memory.py
│ ├── evaluate_agent.py
│ └── generate_evaluation_dataset.py
└── uv.lock
├── philoagents-ui
├── .babelrc
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── index.html
├── log.js
├── package-lock.json
├── package.json
├── public
│ ├── assets
│ │ ├── characters
│ │ │ ├── ada
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── aristotle
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── chomsky
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── dennett
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── descartes
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── leibniz
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── miguel
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── paul
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── plato
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── searle
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── socrates
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ ├── sophia
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ │ └── turing
│ │ │ │ ├── atlas.json
│ │ │ │ └── atlas.png
│ │ ├── game_screenshot.png
│ │ ├── logo.png
│ │ ├── philoagents_town.png
│ │ ├── sprite_image.png
│ │ ├── talking_philosophers.jpg
│ │ ├── tilemaps
│ │ │ └── philoagents-town.json
│ │ └── tilesets
│ │ │ ├── ancient_greece_tileset.png
│ │ │ ├── plant.png
│ │ │ └── tuxmon-sample-32px-extruded.png
│ ├── favicon.png
│ └── style.css
├── src
│ ├── classes
│ │ ├── Character.js
│ │ ├── DialogueBox.js
│ │ └── DialogueManager.js
│ ├── main.js
│ ├── scenes
│ │ ├── Game.js
│ │ ├── MainMenu.js
│ │ ├── PauseMenu.js
│ │ └── Preloader.js
│ └── services
│ │ ├── ApiService.js
│ │ └── WebSocketApiService.js
└── webpack
│ ├── config.js
│ └── config.prod.js
└── static
├── diagrams
├── agent_memory.png
├── episode_1_play.png
├── episode_2_play.png
├── episode_3_play.png
├── episode_4_play.png
├── episode_5_play.png
├── episode_6_play.png
├── langgraph_agent.png
└── system_architecture.png
├── game_socrates_example.png
├── game_starting_page.png
├── logo.png
├── opik_evaluation_example.png
├── opik_monitoring_example.png
├── sponsors
├── groq.png
├── mongo.png
└── opik.png
└── thumbnails
├── episode_1_play.png
├── episode_2_play.png
├── episode_3_play.png
├── episode_4_play.png
├── episode_5_play.png
└── full_course_play.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,python,visualstudiocode,pycharm+all
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,python,visualstudiocode,pycharm+all
3 |
4 | ### macOS ###
5 | # General
6 | .DS_Store
7 | .AppleDouble
8 | .LSOverride
9 |
10 | # Icon must end with two \r
11 | Icon
12 |
13 |
14 | # Thumbnails
15 | ._*
16 |
17 | # Files that might appear in the root of a volume
18 | .DocumentRevisions-V100
19 | .fseventsd
20 | .Spotlight-V100
21 | .TemporaryItems
22 | .Trashes
23 | .VolumeIcon.icns
24 | .com.apple.timemachine.donotpresent
25 |
26 | # Directories potentially created on remote AFP share
27 | .AppleDB
28 | .AppleDesktop
29 | Network Trash Folder
30 | Temporary Items
31 | .apdisk
32 |
33 | ### macOS Patch ###
34 | # iCloud generated files
35 | *.icloud
36 |
37 | ### PyCharm+all ###
38 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
39 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
40 |
41 | # User-specific stuff
42 | .idea/**/workspace.xml
43 | .idea/**/tasks.xml
44 | .idea/**/usage.statistics.xml
45 | .idea/**/dictionaries
46 | .idea/**/shelf
47 |
48 | # AWS User-specific
49 | .idea/**/aws.xml
50 |
51 | # Generated files
52 | .idea/**/contentModel.xml
53 |
54 | # Sensitive or high-churn files
55 | .idea/**/dataSources/
56 | .idea/**/dataSources.ids
57 | .idea/**/dataSources.local.xml
58 | .idea/**/sqlDataSources.xml
59 | .idea/**/dynamic.xml
60 | .idea/**/uiDesigner.xml
61 | .idea/**/dbnavigator.xml
62 |
63 | # Gradle
64 | .idea/**/gradle.xml
65 | .idea/**/libraries
66 |
67 | # Gradle and Maven with auto-import
68 | # When using Gradle or Maven with auto-import, you should exclude module files,
69 | # since they will be recreated, and may cause churn. Uncomment if using
70 | # auto-import.
71 | # .idea/artifacts
72 | # .idea/compiler.xml
73 | # .idea/jarRepositories.xml
74 | # .idea/modules.xml
75 | # .idea/*.iml
76 | # .idea/modules
77 | # *.iml
78 | # *.ipr
79 |
80 | # CMake
81 | cmake-build-*/
82 |
83 | # Mongo Explorer plugin
84 | .idea/**/mongoSettings.xml
85 |
86 | # File-based project format
87 | *.iws
88 |
89 | # IntelliJ
90 | out/
91 |
92 | # mpeltonen/sbt-idea plugin
93 | .idea_modules/
94 |
95 | # JIRA plugin
96 | atlassian-ide-plugin.xml
97 |
98 | # Cursive Clojure plugin
99 | .idea/replstate.xml
100 |
101 | # SonarLint plugin
102 | .idea/sonarlint/
103 |
104 | # Crashlytics plugin (for Android Studio and IntelliJ)
105 | com_crashlytics_export_strings.xml
106 | crashlytics.properties
107 | crashlytics-build.properties
108 | fabric.properties
109 |
110 | # Editor-based Rest Client
111 | .idea/httpRequests
112 |
113 | # Android studio 3.1+ serialized cache file
114 | .idea/caches/build_file_checksums.ser
115 |
116 | ### PyCharm+all Patch ###
117 | # Ignore everything but code style settings and run configurations
118 | # that are supposed to be shared within teams.
119 |
120 | .idea/*
121 |
122 | !.idea/codeStyles
123 | !.idea/runConfigurations
124 |
125 | ### Python ###
126 | # Byte-compiled / optimized / DLL files
127 | __pycache__/
128 | *.py[cod]
129 | *$py.class
130 |
131 | # C extensions
132 | *.so
133 |
134 | # Distribution / packaging
135 | .Python
136 | build/
137 | develop-eggs/
138 | dist/
139 | downloads/
140 | eggs/
141 | .eggs/
142 | lib/
143 | lib64/
144 | parts/
145 | sdist/
146 | var/
147 | wheels/
148 | share/python-wheels/
149 | *.egg-info/
150 | .installed.cfg
151 | *.egg
152 | MANIFEST
153 |
154 | # PyInstaller
155 | # Usually these files are written by a python script from a template
156 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
157 | *.manifest
158 | *.spec
159 |
160 | # Installer logs
161 | pip-log.txt
162 | pip-delete-this-directory.txt
163 |
164 | # Unit test / coverage reports
165 | htmlcov/
166 | .tox/
167 | .nox/
168 | .coverage
169 | .coverage.*
170 | .cache
171 | nosetests.xml
172 | coverage.xml
173 | *.cover
174 | *.py,cover
175 | .hypothesis/
176 | .pytest_cache/
177 | cover/
178 |
179 | # Translations
180 | *.mo
181 | *.pot
182 |
183 | # Django stuff:
184 | *.log
185 | local_settings.py
186 | db.sqlite3
187 | db.sqlite3-journal
188 |
189 | # Flask stuff:
190 | instance/
191 | .webassets-cache
192 |
193 | # Scrapy stuff:
194 | .scrapy
195 |
196 | # Sphinx documentation
197 | docs/_build/
198 |
199 | # PyBuilder
200 | .pybuilder/
201 | target/
202 |
203 | # Jupyter Notebook
204 | .ipynb_checkpoints
205 |
206 | # IPython
207 | profile_default/
208 | ipython_config.py
209 |
210 | # pyenv
211 | # For a library or package, you might want to ignore these files since the code is
212 | # intended to run in multiple environments; otherwise, check them in:
213 | # .python-version
214 |
215 | # pipenv
216 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
217 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
218 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
219 | # install all needed dependencies.
220 | #Pipfile.lock
221 |
222 | # poetry
223 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
224 | # This is especially recommended for binary packages to ensure reproducibility, and is more
225 | # commonly ignored for libraries.
226 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
227 | #poetry.lock
228 |
229 | # pdm
230 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
231 | #pdm.lock
232 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
233 | # in version control.
234 | # https://pdm.fming.dev/#use-with-ide
235 | .pdm.toml
236 |
237 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
238 | __pypackages__/
239 |
240 | # Celery stuff
241 | celerybeat-schedule
242 | celerybeat.pid
243 |
244 | # SageMath parsed files
245 | *.sage.py
246 |
247 | # Environments
248 | .env
249 | .venv
250 | env/
251 | venv/
252 | ENV/
253 | env.bak/
254 | venv.bak/
255 |
256 | # Spyder project settings
257 | .spyderproject
258 | .spyproject
259 |
260 | # Rope project settings
261 | .ropeproject
262 |
263 | # mkdocs documentation
264 | /site
265 |
266 | # mypy
267 | .mypy_cache/
268 | .dmypy.json
269 | dmypy.json
270 |
271 | # Pyre type checker
272 | .pyre/
273 |
274 | # pytype static type analyzer
275 | .pytype/
276 |
277 | # Cython debug symbols
278 | cython_debug/
279 |
280 | # PyCharm
281 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
282 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
283 | # and can be added to the global gitignore or merged into this file. For a more nuclear
284 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
285 | #.idea/
286 |
287 | ### Python Patch ###
288 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
289 | poetry.toml
290 |
291 | # ruff
292 | .ruff_cache/
293 |
294 | # LSP config files
295 | pyrightconfig.json
296 |
297 | ### VisualStudioCode ###
298 | .vscode/*
299 | !.vscode/settings.json
300 | !.vscode/tasks.json
301 | !.vscode/launch.json
302 | !.vscode/extensions.json
303 | !.vscode/*.code-snippets
304 |
305 | # Local History for Visual Studio Code
306 | .history/
307 |
308 | # Built Visual Studio Code Extensions
309 | *.vsix
310 |
311 | ### VisualStudioCode Patch ###
312 | # Ignore all local history of files
313 | .history
314 | .ionide
315 |
316 | # End of https://www.toptal.com/developers/gitignore/api/macos,python,visualstudiocode,pycharm+all
317 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Python Debugger: Current File",
6 | "type": "debugpy",
7 | "request": "launch",
8 | "program": "${file}",
9 | "console": "integratedTerminal",
10 | "cwd": "${workspaceFolder}/philoagents-api",
11 | "justMyCode": false,
12 | "env": {
13 | "MONGO_URI": "mongodb://philoagents:philoagents@localhost:27017/?directConnection=true"
14 | }
15 | },
16 | {
17 | "name": "Create Long Term Memory",
18 | "type": "debugpy",
19 | "request": "launch",
20 | "cwd": "${workspaceFolder}/philoagents-api",
21 | "module": "tools.create_long_term_memory",
22 | "args": [],
23 | "justMyCode": false,
24 | "env": {
25 | "MONGO_URI": "mongodb://philoagents:philoagents@localhost:27017/?directConnection=true"
26 | }
27 | },
28 | {
29 | "name": "Evaluate Agent",
30 | "type": "debugpy",
31 | "request": "launch",
32 | "cwd": "${workspaceFolder}/philoagents-api",
33 | "module": "tools.evaluate_agent",
34 | "args": [],
35 | "justMyCode": false,
36 | "env": {
37 | "MONGO_URI": "mongodb://philoagents:philoagents@localhost:27017/?directConnection=true"
38 | }
39 | },
40 | {
41 | "name": "Generate Evaluation Dataset",
42 | "type": "debugpy",
43 | "request": "launch",
44 | "cwd": "${workspaceFolder}/philoagents-api",
45 | "module": "tools.generate_evaluation_dataset",
46 | "args": [],
47 | "justMyCode": false,
48 | "env": {
49 | "MONGO_URI": "mongodb://philoagents:philoagents@localhost:27017/?directConnection=true"
50 | }
51 | }
52 | ]
53 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[python]": {
3 | "editor.formatOnSave": true,
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll": "explicit",
6 | "source.organizeImports": "explicit"
7 | },
8 | "editor.defaultFormatter": "charliermarsh.ruff"
9 | },
10 | "notebook.formatOnSave.enabled": true,
11 | "notebook.codeActionsOnSave": {
12 | "notebook.source.fixAll": true,
13 | "notebook.source.organizeImports": true
14 | },
15 | "python.defaultInterpreterPath": "philoagents-api/.venv/bin/python"
16 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the PhiloAgents Course
2 |
3 | Welcome to one of the most comprehensive open-source courses on Agents 👋
4 |
5 | We deeply appreciate your support for the AI community and future readers 🤗
6 |
7 | ## Ways to Contribute
8 |
9 | The course itself is already a comprehensive end-to-end resource.
10 |
11 | But as a community project, we weren't able to test the code on all possible scenarios.
12 |
13 | Thus, if you find any bugs or improvements, consider supporting future readers by contributing to this course.
14 |
15 | A contribution can be:
16 | - Fixing typos
17 | - Updating version numbers
18 | - Fixing fundamental issues, such as Python modules that don't work anymore
19 | - Clarification in documentation
20 | - Support for different operating systems (e.g., Windows)
21 |
22 | Remember, no contribution is too small. Every improvement helps make this repository an even better resource for the community.
23 |
24 | ## Reporting Issues
25 |
26 | Found a problem or have a suggestion? Please create an issue on GitHub, providing as much detail as possible.
27 |
28 | ## Contributing Code or Content
29 |
30 | 1. **Fork & Branch:** Fork the repo and create a branch from `main`.
31 | 2. **Make Changes:** Implement your contribution.
32 | 3. **Test:** Verify your changes work properly.
33 | 4. **Follow Style:** Match existing code and documentation conventions.
34 | 5. **Commit:** Write clear, concise commit messages.
35 | 6. **Stay Updated:** Ensure your code is updated with the main branch before submitting.
36 | 7. **Submit PR:** Push to your fork and open a pull request.
37 | 8. **Review Process:** Wait for maintainer review.
38 | 9. **Merge:** Approved changes will be merged to main.
39 |
40 | 📍 [Official Guide on creating a pull request from a forked GitHub repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) or use an LLM for more detailed instructions.
41 |
42 | Congratulations! You're now a contributor to the PhiloAgents open-source course. 🔥
43 |
44 | ## Code Quality and Readability
45 |
46 | For high-quality, readable code:
47 | - Write clean, well-structured code
48 | - Add helpful comments for complex logic
49 | - Use consistent formatting
50 | - Use consistent documentation style
51 | - Consider using a language model to improve readability
52 |
53 | ## Final Notes
54 |
55 | We're grateful for all contributors. Your work helps future readers and the AI community.
56 |
57 | Let's make the AI community better together! 🤘
58 |
--------------------------------------------------------------------------------
/INSTALL_AND_USAGE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
📬 Stay Updated
11 |
Join The Neural Maze and learn to build AI Systems that actually work, from principles to production. Every Wednesday, directly to your inbox. Don't miss out!
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
📬 Stay Updated
34 |
Join Decoding ML for proven content on designing, coding, and deploying production-grade AI systems with software engineering and MLOps best practices to help you ship AI applications. Every week, straight to your inbox.
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | ------
47 |
48 | # 🚀 Installation and Usage Guide
49 |
50 | This guide will help you set up and run a ...
51 |
52 | # 📑 Table of Contents
53 |
54 | - [📋 Prerequisites](#-prerequisites)
55 | - [🎯 Getting Started](#-getting-started)
56 | - [📁 Project Structure](#-project-structure)
57 | - [🏗️ Set Up Your Local Infrastructure](#-set-up-your-local-infrastructure)
58 | - [⚡️ Running the Code for Each Module](#️-running-the-code-for-each-module)
59 | - [🔧 Utlity Commands](#-utility-commands)
60 |
61 | # 📋 Prerequisites
62 |
63 | ## Local Tools
64 |
65 | For all the modules, you'll need the following tools installed locally:
66 |
67 | | Tool | Version | Purpose | Installation Link |
68 | |------|---------|---------|------------------|
69 | | Python | 3.11 | Programming language runtime | [Download](https://www.python.org/downloads/) |
70 | | uv | ≥ 0.4.30 | Python package installer and virtual environment manager | [Download](https://github.com/astral-sh/uv) |
71 | | GNU Make | ≥ 3.81 | Build automation tool | [Download](https://www.gnu.org/software/make/) |
72 | | Git | ≥2.44.0 | Version control | [Download](https://git-scm.com/downloads) |
73 | | Docker | ≥27.4.0 | Containerization platform | [Download](https://www.docker.com/get-started/) |
74 |
75 |
76 | 📌 Windows users also need to install WSL (Click to expand)
77 |
78 | We will be using Unix commands across the course, so if you are using Windows, you will need to **install WSL**, which will install a Linux kernel on your Windows machine and allow you to use the Unix commands from our course (this is the recommended way to write software on Windows).
79 |
80 | 🔗 [Follow this guide to install WSL](https://www.youtube.com/watch?v=YByZ_sOOWsQ).
81 |
82 |
83 | ## Cloud Services
84 |
85 | Also, the course requires access to these cloud services. The authentication to these services is done by adding the corresponding environment variables to the `.env` file:
86 |
87 | | Service | Purpose | Cost | Environment Variable | Setup Guide | Starting with Module |
88 | |---------|---------|------|---------------------|-------------| ---------------------|
89 | | [Groq](https://rebrand.ly/philoagents-groq) | LLM API that powers the agents | Free tier | `GROQ_API_KEY` | [Quick Start Guide](https://rebrand.ly/philoagents-groq-quickstart) | Module 1 |
90 | | [Opik](https://rebrand.ly/philoagents-opik) | LLMOps | Free tier (Hosted on Comet - same API Key) | `COMET_API_KEY` | [Quick Start Guide](https://rebrand.ly/philoagents-opik-quickstart) | Module 5 |
91 | | [OpenAI API](https://openai.com/index/openai-api/) | LLM API used for evaluation | Pay-per-use | `OPENAI_API_KEY` | [Quick Start Guide](https://platform.openai.com/docs/quickstart) | Module 5 |
92 |
93 | When working locally, the infrastructure is set up using Docker. Thus, you can use the default values found in the [config.py](philoagents-api/src/philoagents/config.py) file for all the infrastructure-related environment variables.
94 |
95 | But, in case you want to deploy the code, you'll need to setup the following services with their corresponding environment variables:
96 |
97 | | Service | Purpose | Cost | Required Credentials | Setup Guide |
98 | |---------|---------|------|---------------------|-------------|
99 | | [MongoDB](https://rebrand.ly/philoagents-mongodb) | Document database | Free tier | `MONGODB_URI` | 1. [Create a free MongoDB Atlas account](https://rebrand.ly/philoagents-mongodb-setup-1) 2. [Create a Cluster](https://rebrand.ly/philoagents-mongodb-setup-2) 3. [Add a Database User](https://rebrand.ly/philoagents-mongodb-setup-3) 4. [Configure a Network Connection](https://rebrand.ly/philoagents-mongodb-setup-4) |
100 |
101 | # 🎯 Getting Started
102 |
103 | ## 1. Clone the Repository
104 |
105 | Start by cloning the repository and navigating to the `philoagents-api` project directory:
106 | ```
107 | git clone https://github.com/neural-maze/philoagents-course.git
108 | cd philoagents-course/philoagents-api
109 | ```
110 |
111 | Next, we have to prepare your Python environment and its dependencies.
112 |
113 | ## 2. Installation
114 |
115 | Inside the `philoagents-api` directory, to install the dependencies and activate the virtual environment, run the following commands:
116 |
117 | ```bash
118 | uv venv .venv
119 | . ./.venv/bin/activate # or source ./.venv/bin/activate
120 | uv pip install -e .
121 | ```
122 |
123 | Test that you have Python 3.11.9 installed in your new `uv` environment:
124 | ```bash
125 | uv run python --version
126 | # Output: Python 3.11.9
127 | ```
128 |
129 | This command will:
130 | - Create a virtual environment with the Python version specified in `.python-version` using `uv`
131 | - Activate the virtual environment
132 | - Install all dependencies from `pyproject.toml`
133 |
134 | ## 3. Environment Configuration
135 |
136 | Before running any command, inside the `philoagents-api` directory, you have to set up your environment:
137 | 1. Create your environment file:
138 | ```bash
139 | cp .env.example .env
140 | ```
141 | 2. Open `.env` and configure the required credentials following the inline comments and the recommendations from the [Cloud Services](#-prerequisites) section.
142 |
143 | # 📁 Project Structure
144 |
145 | The project follows a clean architecture structure commonly used in production Python projects:
146 |
147 | ```bash
148 | philoagents-api/
149 | ├── data/ # Data files
150 | ├── notebooks/ # Notebooks
151 | ├── src/philoagents/ # Main package directory
152 | │ ├── application/ # Application layer
153 | │ ├── domain/ # Domain layer
154 | │ ├── infrastructure/ # Infrastructure layer
155 | │ └── config.py # Configuration settings
156 | ├── tools/ # Entrypoint scripts that use the Python package
157 | ├── .env.example # Environment variables template
158 | ├── .python-version # Python version specification
159 | ├── Dockerfile # API Docker image definition
160 | ├── Makefile # Project commands
161 | └── pyproject.toml # Project dependencies
162 | ```
163 |
164 | # 🏗️ Set Up Your Local Infrastructure
165 |
166 | We use Docker to set up the local infrastructure (Game UI, Agent API, MongoDB).
167 |
168 | > [!WARNING]
169 | > Before running the command below, ensure you do not have any processes running on ports `27017` (MongoDB), `8000` (Agent API) and `8080` (Game UI).
170 |
171 | From the root `philoagents-course` directory, to start the Docker infrastructure, run:
172 | ```bash
173 | make infrastructure-up
174 | ```
175 |
176 | From the root `philoagents-course` directory, to stop the Docker infrastructure, run:
177 | ```bash
178 | make infrastructure-stop
179 | ```
180 |
181 | From the root `philoagents-course` directory, to build the Docker images (without running them), run:
182 | ```bash
183 | make infrastructure-build
184 | ```
185 |
186 | # ⚡️ Running the Code for Each Lesson
187 |
188 | After you have set up your environment (through the `.env` file) and local infrastructure (through Docker), you are ready to run and test out the game simulation.
189 |
190 | ## Modules 1, 2, 3, 4 and 6
191 |
192 | As most of the modules are coupled, you must test them all at once.
193 |
194 | First, from the root `philoagents-course` directory, populate the long term memory within your MongoDB instance (required for agentic RAG) with the following command:
195 | ```bash
196 | make create-long-term-memory
197 | ```
198 |
199 | > [!NOTE]
200 | > To visualize the raw and RAG data from MongoDB, we recommend using [MongoDB Compass](https://rebrand.ly/philoagents-mongodb-compass) or Mongo's official IDE plugin (e.g., `MongoDB for VS Code`). To connect to the working MongoDB instance, use the `MONGODB_URI` value from the `.env` file or found inside the [config.py](philoagents-api/src/philoagents/config.py) file.
201 |
202 | Next, you can access the game by typing in your browser:
203 | ```
204 | http://localhost:8080
205 | ```
206 | Which will open the game UI, similar to the screenshot below:
207 |
208 | 
209 |
210 | To see the instructions for playing the game, you can click on the `Instructions` button. Click the `Let's Play!` button to start the game.
211 |
212 | Now you can start playing the game, wander around the town and talk to our philosophers, as seen in the screenshot below:
213 |
214 | 
215 |
216 | You can also access the API documentation by typing in your browser:
217 | ```
218 | http://localhost:8000/docs
219 | ```
220 |
221 | If you want to **directly call the agent bypassing the backend and UI logic**, you can do that by running:
222 | ```bash
223 | make call-agent
224 | ```
225 |
226 | To delete the long term memory from your MongoDB instance, you can run the following command:
227 | ```bash
228 | make delete-long-term-memory
229 | ```
230 |
231 | ## Module 5
232 |
233 | Only module 5 on evaluation and monitoring has its own instructions.
234 |
235 | First, to visualize the prompt traces, as seen in the screenshot below, visit [Opik](https://rebrand.ly/philoagents-opik-dashboard).
236 |
237 | 
238 |
239 | To evaluate the agents, from the root `philoagents-course` directory, you can run the following command:
240 | ```bash
241 | make evaluate-agent
242 | ```
243 |
244 | To visualize the evaluation results, as seen in the screenshot below, you also have to visit [Opik](https://rebrand.ly/philoagents-opik-dashboard).
245 |
246 | 
247 |
248 | We already generated a dataset for you found at [data/evaluation_dataset.json](philoagents-api/data/evaluation_dataset.json), but in case you want to generate a new one (to override the existing one), you can run the following command:
249 | ```bash
250 | make generate-evaluation-dataset
251 | ```
252 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 The Neural Maze
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ifeq (,$(wildcard philoagents-api/.env))
2 | $(error .env file is missing at philoagents-api/.env. Please create one based on .env.example)
3 | endif
4 |
5 | include philoagents-api/.env
6 |
7 | # --- Infrastructure ---
8 |
9 | infrastructure-build:
10 | docker compose build
11 |
12 | infrastructure-up:
13 | docker compose up --build -d
14 |
15 | infrastructure-stop:
16 | docker compose stop
17 |
18 | check-docker-image:
19 | @if [ -z "$$(docker images -q philoagents-course-api 2> /dev/null)" ]; then \
20 | echo "Error: philoagents-course-api Docker image not found."; \
21 | echo "Please run 'make infrastructure-build' first to build the required images."; \
22 | exit 1; \
23 | fi
24 |
25 | # --- Offline Pipelines ---
26 |
27 | call-agent: check-docker-image
28 | docker run --rm --network=philoagents-network --env-file philoagents-api/.env -v ./philoagents-api/data:/app/data philoagents-course-api uv run python -m tools.call_agent --philosopher-id "turing" --query "How can we know the difference between a human and a machine?"
29 |
30 | create-long-term-memory: check-docker-image
31 | docker run --rm --network=philoagents-network --env-file philoagents-api/.env -v ./philoagents-api/data:/app/data philoagents-course-api uv run python -m tools.create_long_term_memory
32 |
33 | delete-long-term-memory: check-docker-image
34 | docker run --rm --network=philoagents-network --env-file philoagents-api/.env philoagents-course-api uv run python -m tools.delete_long_term_memory
35 |
36 | generate-evaluation-dataset: check-docker-image
37 | docker run --rm --network=philoagents-network --env-file philoagents-api/.env -v ./philoagents-api/data:/app/data philoagents-course-api uv run python -m tools.generate_evaluation_dataset --max-samples 15
38 |
39 | evaluate-agent: check-docker-image
40 | docker run --rm --network=philoagents-network --env-file philoagents-api/.env -v ./philoagents-api/data:/app/data philoagents-course-api uv run python -m tools.evaluate_agent --workers 1 --nb-samples 15
41 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | local_dev_atlas:
3 | image: mongodb/mongodb-atlas-local:8.0
4 | hostname: local_dev_atlas
5 | ports:
6 | - 27017:27017
7 | environment:
8 | - MONGODB_INITDB_ROOT_USERNAME=philoagents
9 | - MONGODB_INITDB_ROOT_PASSWORD=philoagents
10 | volumes:
11 | - data:/data/db
12 | - config:/data/configdb
13 | networks:
14 | - philoagents-network
15 | api:
16 | container_name: philoagents-api
17 | build:
18 | context: ./philoagents-api
19 | dockerfile: Dockerfile
20 | environment:
21 | - MONGODB_URI=mongodb://philoagents:philoagents@local_dev_atlas:27017/?directConnection=true
22 | ports:
23 | - "8000:8000"
24 | env_file:
25 | - ./philoagents-api/.env
26 | networks:
27 | - philoagents-network
28 | ui:
29 | container_name: philoagents-ui
30 | build:
31 | context: ./philoagents-ui
32 | dockerfile: Dockerfile
33 | ports:
34 | - "8080:8080"
35 | volumes:
36 | - ./philoagents-ui:/app
37 | - /app/node_modules
38 | depends_on:
39 | - api
40 | networks:
41 | - philoagents-network
42 |
43 | volumes:
44 | data:
45 | config:
46 |
47 | networks:
48 | philoagents-network:
49 | name: philoagents-network
--------------------------------------------------------------------------------
/philoagents-api/.dockerignore:
--------------------------------------------------------------------------------
1 | data/
2 | .venv/
3 | .env
4 | .DS_Store
5 | .vscode/
6 | .ruff_cache/
7 | img/
--------------------------------------------------------------------------------
/philoagents-api/.env.example:
--------------------------------------------------------------------------------
1 | # Required with Module 1
2 | GROQ_API_KEY=
3 |
4 | # Required with Module 5 (optional: for evaluation). Make sure to restart the Docker infrastructure after setting this up.
5 | OPENAI_API_KEY=
6 |
7 | # Required with Module 5 (optional: for evaluation and LLMOps). Make sure to restart the Docker infrastructure after setting this up.
8 | COMET_API_KEY=
9 |
--------------------------------------------------------------------------------
/philoagents-api/.python-version:
--------------------------------------------------------------------------------
1 | 3.11.9
2 |
--------------------------------------------------------------------------------
/philoagents-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim
2 |
3 | # Install uv.
4 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
5 |
6 | # Set the working directory.
7 | WORKDIR /app
8 |
9 | # Install the application dependencies.
10 | COPY uv.lock pyproject.toml README.md ./
11 | RUN uv sync --frozen --no-cache
12 |
13 | # Copy the application into the container.
14 | COPY src/philoagents philoagents/
15 | COPY tools tools/
16 |
17 | CMD ["/app/.venv/bin/fastapi", "run", "philoagents/infrastructure/api.py", "--port", "8000", "--host", "0.0.0.0"]
18 |
--------------------------------------------------------------------------------
/philoagents-api/Makefile:
--------------------------------------------------------------------------------
1 | ifeq (,$(wildcard .env))
2 | $(error .env file is missing. Please create one based on .env.example)
3 | endif
4 |
5 | include .env
6 |
7 | CHECK_DIRS := .
8 |
9 | # --- QA ---
10 |
11 | format-fix:
12 | uv run ruff format $(CHECK_DIRS)
13 | uv run ruff check --select I --fix
14 |
15 | lint-fix:
16 | uv run ruff check --fix
17 |
18 | format-check:
19 | uv run ruff format --check $(CHECK_DIRS)
20 | uv run ruff check -e
21 | uv run ruff check --select I -e
22 |
23 | lint-check:
24 | uv run ruff check $(CHECK_DIRS)
25 |
--------------------------------------------------------------------------------
/philoagents-api/README.md:
--------------------------------------------------------------------------------
1 | # PhiloAgents API
2 |
3 | Check the [INSTALL_AND_USAGE.md](../INSTALL_AND_USAGE.md) file for instructions on how to install and use the API.
4 |
5 | # 🔧 Utlity Commands
6 |
7 | ## Formatting
8 |
9 | ```
10 | make format-check
11 | make format-fix
12 | ```
13 |
14 | ## Linting
15 |
16 | ```bash
17 | make lint-check
18 | make lint-fix
19 | ```
20 |
21 | ## Tests
22 |
23 | ```bash
24 | make test
25 | ```
--------------------------------------------------------------------------------
/philoagents-api/data/extraction_metadata.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "plato",
4 | "urls": [
5 | "https://plato.stanford.edu/entries/plato/"
6 | ]
7 | },
8 | {
9 | "id": "aristotle",
10 | "urls": [
11 | "https://plato.stanford.edu/entries/aristotle/"
12 | ]
13 | },
14 | {
15 | "id": "socrates",
16 | "urls": [
17 | "https://plato.stanford.edu/entries/socrates/"
18 | ]
19 | },
20 | {
21 | "id": "descartes",
22 | "urls": [
23 | "https://plato.stanford.edu/entries/descartes/"
24 | ]
25 | },
26 | {
27 | "id": "leibniz",
28 | "urls": [
29 | "https://plato.stanford.edu/entries/leibniz/"
30 | ]
31 | },
32 | {
33 | "id": "ada_lovelace",
34 | "urls": []
35 | },
36 | {
37 | "id": "turing",
38 | "urls": [
39 | "https://plato.stanford.edu/entries/turing/"
40 | ]
41 | },
42 | {
43 | "id": "searle",
44 | "urls": []
45 | },
46 | {
47 | "id": "chomsky",
48 | "urls": []
49 | },
50 | {
51 | "id": "dennett",
52 | "urls": []
53 | }
54 | ]
--------------------------------------------------------------------------------
/philoagents-api/langgraph.json:
--------------------------------------------------------------------------------
1 | {
2 | "dockerfile_lines": [],
3 | "graphs": {
4 | "agent": "./src/philoagents/application/conversation_service/workflow/graph.py:graph"
5 | },
6 | "env": ".env",
7 | "python_version": "3.12",
8 | "dependencies": [
9 | "."
10 | ]
11 | }
--------------------------------------------------------------------------------
/philoagents-api/notebooks/long_term_memory_in_action.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from langchain_core.documents import Document\n",
10 | "\n",
11 | "from philoagents.application import LongTermMemoryRetriever"
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": 5,
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "from philoagents.config import settings\n",
21 | "\n",
22 | "# Override MongoDB connection string\n",
23 | "settings.MONGO_URI = (\n",
24 | " \"mongodb://philoagents:philoagents@localhost:27017/?directConnection=true\"\n",
25 | ")"
26 | ]
27 | },
28 | {
29 | "cell_type": "code",
30 | "execution_count": 6,
31 | "metadata": {},
32 | "outputs": [],
33 | "source": [
34 | "def print_memories(memories: list[Document]) -> None:\n",
35 | " for i, memory in enumerate(memories):\n",
36 | " print(\"-\" * 100)\n",
37 | " print(f\"Memory {i + 1}:\")\n",
38 | " print(f\"{i + 1}. {memory.page_content[:100]}\")\n",
39 | " print(f\"Source: {memory.metadata['source']}\")\n",
40 | " print(\"-\" * 100)"
41 | ]
42 | },
43 | {
44 | "cell_type": "code",
45 | "execution_count": null,
46 | "metadata": {},
47 | "outputs": [],
48 | "source": [
49 | "retriever = LongTermMemoryRetriever.build_from_settings()\n",
50 | "\n",
51 | "memories = retriever(\"Socrates\")\n",
52 | "print_memories(memories)"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "memories = retriever(\"Turing\")\n",
62 | "print_memories(memories)"
63 | ]
64 | }
65 | ],
66 | "metadata": {
67 | "kernelspec": {
68 | "display_name": ".venv",
69 | "language": "python",
70 | "name": "python3"
71 | },
72 | "language_info": {
73 | "codemirror_mode": {
74 | "name": "ipython",
75 | "version": 3
76 | },
77 | "file_extension": ".py",
78 | "mimetype": "text/x-python",
79 | "name": "python",
80 | "nbconvert_exporter": "python",
81 | "pygments_lexer": "ipython3",
82 | "version": "3.11.9"
83 | }
84 | },
85 | "nbformat": 4,
86 | "nbformat_minor": 2
87 | }
88 |
--------------------------------------------------------------------------------
/philoagents-api/notebooks/short_term_memory_in_action.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 26,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from langchain_core.messages import HumanMessage\n",
10 | "from langgraph.checkpoint.mongodb.aio import AsyncMongoDBSaver\n",
11 | "\n",
12 | "from philoagents.application.conversation_service.workflow.graph import (\n",
13 | " create_workflow_graph,\n",
14 | ")\n",
15 | "from philoagents.config import settings\n",
16 | "\n",
17 | "from philoagents.domain.philosopher import Philosopher\n",
18 | "\n",
19 | "# Override MongoDB connection string\n",
20 | "settings.MONGO_URI = (\n",
21 | " \"mongodb://philoagents:philoagents@localhost:27017/?directConnection=true\"\n",
22 | ")"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": 51,
28 | "metadata": {},
29 | "outputs": [],
30 | "source": [
31 | "async def generate_response_without_memory(philosopher: Philosopher, messages: list):\n",
32 | " graph = graph_builder.compile()\n",
33 | " output_state = await graph.ainvoke(\n",
34 | " input={\n",
35 | " \"messages\": messages,\n",
36 | " \"philosopher_name\": philosopher.name,\n",
37 | " \"philosopher_perspective\": philosopher.perspective,\n",
38 | " \"philosopher_style\": philosopher.style,\n",
39 | " \"philosopher_context\": \"\",\n",
40 | " },\n",
41 | " )\n",
42 | " last_message = output_state[\"messages\"][-1]\n",
43 | " return last_message\n",
44 | "\n",
45 | "async def generate_response_with_memory(philosopher: Philosopher, messages: list):\n",
46 | " async with AsyncMongoDBSaver.from_conn_string(\n",
47 | " conn_string=settings.MONGO_URI,\n",
48 | " db_name=settings.MONGO_DB_NAME,\n",
49 | " checkpoint_collection_name=settings.MONGO_STATE_CHECKPOINT_COLLECTION,\n",
50 | " writes_collection_name=settings.MONGO_STATE_WRITES_COLLECTION,\n",
51 | " ) as checkpointer:\n",
52 | " graph = graph_builder.compile(checkpointer=checkpointer)\n",
53 | "\n",
54 | " config = {\n",
55 | " \"configurable\": {\"thread_id\": philosopher.id},\n",
56 | " }\n",
57 | " output_state = await graph.ainvoke(\n",
58 | " input={\n",
59 | " \"messages\": messages,\n",
60 | " \"philosopher_name\": philosopher.name,\n",
61 | " \"philosopher_perspective\": philosopher.perspective,\n",
62 | " \"philosopher_style\": philosopher.style,\n",
63 | " \"philosopher_context\": \"\",\n",
64 | " },\n",
65 | " config=config,\n",
66 | " )\n",
67 | " \n",
68 | " last_message = output_state[\"messages\"][-1]\n",
69 | " return last_message"
70 | ]
71 | },
72 | {
73 | "cell_type": "markdown",
74 | "metadata": {},
75 | "source": [
76 | "### PhiloAgent without short term memory"
77 | ]
78 | },
79 | {
80 | "cell_type": "markdown",
81 | "metadata": {},
82 | "source": [
83 | "First of all, we need to create the graph builder."
84 | ]
85 | },
86 | {
87 | "cell_type": "code",
88 | "execution_count": 45,
89 | "metadata": {},
90 | "outputs": [],
91 | "source": [
92 | "graph_builder = create_workflow_graph()"
93 | ]
94 | },
95 | {
96 | "cell_type": "markdown",
97 | "metadata": {},
98 | "source": [
99 | "Now, just create a test PhiloAgent."
100 | ]
101 | },
102 | {
103 | "cell_type": "code",
104 | "execution_count": 55,
105 | "metadata": {},
106 | "outputs": [],
107 | "source": [
108 | "test_philosopher = Philosopher(\n",
109 | " id=\"andrej_karpathy\",\n",
110 | " name=\"Andrej Karpathy\",\n",
111 | " perspective=\"He is the goat of AI and asks you about your proficiency in C and GPU programming\",\n",
112 | " style=\"He is very friendly and engaging, and he is very good at explaining things\"\n",
113 | ")"
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": 56,
119 | "metadata": {},
120 | "outputs": [],
121 | "source": [
122 | "messages = [\n",
123 | " HumanMessage(content=\"Hello, my name is Miguel\")\n",
124 | "]"
125 | ]
126 | },
127 | {
128 | "cell_type": "code",
129 | "execution_count": null,
130 | "metadata": {},
131 | "outputs": [],
132 | "source": [
133 | "await generate_response_without_memory(test_philosopher, messages)"
134 | ]
135 | },
136 | {
137 | "cell_type": "code",
138 | "execution_count": 58,
139 | "metadata": {},
140 | "outputs": [],
141 | "source": [
142 | "messages = [\n",
143 | " HumanMessage(content=\"Do you know my name?\")\n",
144 | "]"
145 | ]
146 | },
147 | {
148 | "cell_type": "code",
149 | "execution_count": null,
150 | "metadata": {},
151 | "outputs": [],
152 | "source": [
153 | "await generate_response_without_memory(test_philosopher, messages)"
154 | ]
155 | },
156 | {
157 | "cell_type": "markdown",
158 | "metadata": {},
159 | "source": [
160 | "### PhiloAgent with short term memory"
161 | ]
162 | },
163 | {
164 | "cell_type": "code",
165 | "execution_count": 60,
166 | "metadata": {},
167 | "outputs": [],
168 | "source": [
169 | "test_philosopher = Philosopher(\n",
170 | " id=\"andrej_karpathy\",\n",
171 | " name=\"Andrej Karpathy\",\n",
172 | " perspective=\"He is the goat of AI and asks you about your proficiency in C and GPU programming\",\n",
173 | " style=\"He is very friendly and engaging, and he is very good at explaining things\"\n",
174 | ")"
175 | ]
176 | },
177 | {
178 | "cell_type": "code",
179 | "execution_count": 61,
180 | "metadata": {},
181 | "outputs": [],
182 | "source": [
183 | "messages = [\n",
184 | " HumanMessage(content=\"Hello, my name is Miguel\")\n",
185 | "]"
186 | ]
187 | },
188 | {
189 | "cell_type": "code",
190 | "execution_count": null,
191 | "metadata": {},
192 | "outputs": [],
193 | "source": [
194 | "await generate_response_with_memory(test_philosopher, messages)"
195 | ]
196 | },
197 | {
198 | "cell_type": "code",
199 | "execution_count": 63,
200 | "metadata": {},
201 | "outputs": [],
202 | "source": [
203 | "messages = [\n",
204 | " HumanMessage(content=\"Do you know my name?\")\n",
205 | "]"
206 | ]
207 | },
208 | {
209 | "cell_type": "code",
210 | "execution_count": null,
211 | "metadata": {},
212 | "outputs": [],
213 | "source": [
214 | "await generate_response_with_memory(test_philosopher, messages)"
215 | ]
216 | }
217 | ],
218 | "metadata": {
219 | "kernelspec": {
220 | "display_name": ".venv",
221 | "language": "python",
222 | "name": "python3"
223 | },
224 | "language_info": {
225 | "codemirror_mode": {
226 | "name": "ipython",
227 | "version": 3
228 | },
229 | "file_extension": ".py",
230 | "mimetype": "text/x-python",
231 | "name": "python",
232 | "nbconvert_exporter": "python",
233 | "pygments_lexer": "ipython3",
234 | "version": "3.11.9"
235 | }
236 | },
237 | "nbformat": 4,
238 | "nbformat_minor": 2
239 | }
240 |
--------------------------------------------------------------------------------
/philoagents-api/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "philoagents"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | license = { text = "MIT" }
7 | requires-python = ">=3.11"
8 | dependencies = [
9 | "fastapi[standard]>=0.115.8",
10 | "langchain-core>=0.3.34",
11 | "langchain-groq>=0.2.4",
12 | "langchain-mongodb>=0.4.0",
13 | "langgraph>=0.2.70",
14 | "langgraph-checkpoint-mongodb>=0.1.0",
15 | "opik>=1.4.11",
16 | "pre-commit>=4.1.0",
17 | "pydantic-settings>=2.7.1",
18 | "pymongo>=4.9.2",
19 | "loguru>=0.7.3",
20 | "langchain-huggingface>=0.1.2",
21 | "langchain-community>=0.3.17",
22 | "wikipedia>=1.4.0",
23 | "ipykernel>=6.29.5",
24 | "pydantic>=2.10.6",
25 | "datasketch>=1.6.5",
26 | ]
27 |
28 | [dependency-groups]
29 | dev = [
30 | "pytest>=8.3.4",
31 | "ruff>=0.7.2",
32 | ]
33 |
34 | [tool.pip]
35 | extra-index-url = "https://download.pytorch.org/whl/cpu/torch_stable.html"
36 |
37 | [build-system]
38 | requires = ["hatchling"]
39 | build-backend = "hatchling.build"
40 |
41 | [tool.hatch.build.targets.wheel]
42 | packages = ["src/philoagents"]
43 |
44 | [tool.ruff]
45 | target-version = "py312"
46 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/__init__.py:
--------------------------------------------------------------------------------
1 | from philoagents.infrastructure.opik_utils import configure
2 |
3 | configure()
4 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/__init__.py:
--------------------------------------------------------------------------------
1 | from .long_term_memory import LongTermMemoryCreator, LongTermMemoryRetriever
2 |
3 | __all__ = [
4 | "LongTermMemoryCreator",
5 | "LongTermMemoryRetriever",
6 | ]
7 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-api/src/philoagents/application/conversation_service/__init__.py
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/generate_response.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import Any, AsyncGenerator, Union
3 |
4 | from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
5 | from langgraph.checkpoint.mongodb.aio import AsyncMongoDBSaver
6 | from opik.integrations.langchain import OpikTracer
7 |
8 | from philoagents.application.conversation_service.workflow.graph import (
9 | create_workflow_graph,
10 | )
11 | from philoagents.application.conversation_service.workflow.state import PhilosopherState
12 | from philoagents.config import settings
13 |
14 |
15 | async def get_response(
16 | messages: str | list[str] | list[dict[str, Any]],
17 | philosopher_id: str,
18 | philosopher_name: str,
19 | philosopher_perspective: str,
20 | philosopher_style: str,
21 | philosopher_context: str,
22 | new_thread: bool = False,
23 | ) -> tuple[str, PhilosopherState]:
24 | """Run a conversation through the workflow graph.
25 |
26 | Args:
27 | message: Initial message to start the conversation.
28 | philosopher_id: Unique identifier for the philosopher.
29 | philosopher_name: Name of the philosopher.
30 | philosopher_perspective: Philosopher's perspective on the topic.
31 | philosopher_style: Style of conversation (e.g., "Socratic").
32 | philosopher_context: Additional context about the philosopher.
33 |
34 | Returns:
35 | tuple[str, PhilosopherState]: A tuple containing:
36 | - The content of the last message in the conversation.
37 | - The final state after running the workflow.
38 |
39 | Raises:
40 | RuntimeError: If there's an error running the conversation workflow.
41 | """
42 |
43 | graph_builder = create_workflow_graph()
44 |
45 | try:
46 | async with AsyncMongoDBSaver.from_conn_string(
47 | conn_string=settings.MONGO_URI,
48 | db_name=settings.MONGO_DB_NAME,
49 | checkpoint_collection_name=settings.MONGO_STATE_CHECKPOINT_COLLECTION,
50 | writes_collection_name=settings.MONGO_STATE_WRITES_COLLECTION,
51 | ) as checkpointer:
52 | graph = graph_builder.compile(checkpointer=checkpointer)
53 | opik_tracer = OpikTracer(graph=graph.get_graph(xray=True))
54 |
55 | thread_id = (
56 | philosopher_id if not new_thread else f"{philosopher_id}-{uuid.uuid4()}"
57 | )
58 | config = {
59 | "configurable": {"thread_id": thread_id},
60 | "callbacks": [opik_tracer],
61 | }
62 | output_state = await graph.ainvoke(
63 | input={
64 | "messages": __format_messages(messages=messages),
65 | "philosopher_name": philosopher_name,
66 | "philosopher_perspective": philosopher_perspective,
67 | "philosopher_style": philosopher_style,
68 | "philosopher_context": philosopher_context,
69 | },
70 | config=config,
71 | )
72 | last_message = output_state["messages"][-1]
73 | return last_message.content, PhilosopherState(**output_state)
74 | except Exception as e:
75 | raise RuntimeError(f"Error running conversation workflow: {str(e)}") from e
76 |
77 |
78 | async def get_streaming_response(
79 | messages: str | list[str] | list[dict[str, Any]],
80 | philosopher_id: str,
81 | philosopher_name: str,
82 | philosopher_perspective: str,
83 | philosopher_style: str,
84 | philosopher_context: str,
85 | new_thread: bool = False,
86 | ) -> AsyncGenerator[str, None]:
87 | """Run a conversation through the workflow graph with streaming response.
88 |
89 | Args:
90 | messages: Initial message to start the conversation.
91 | philosopher_id: Unique identifier for the philosopher.
92 | philosopher_name: Name of the philosopher.
93 | philosopher_perspective: Philosopher's perspective on the topic.
94 | philosopher_style: Style of conversation (e.g., "Socratic").
95 | philosopher_context: Additional context about the philosopher.
96 | new_thread: Whether to create a new conversation thread.
97 |
98 | Yields:
99 | Chunks of the response as they become available.
100 |
101 | Raises:
102 | RuntimeError: If there's an error running the conversation workflow.
103 | """
104 | graph_builder = create_workflow_graph()
105 |
106 | try:
107 | async with AsyncMongoDBSaver.from_conn_string(
108 | conn_string=settings.MONGO_URI,
109 | db_name=settings.MONGO_DB_NAME,
110 | checkpoint_collection_name=settings.MONGO_STATE_CHECKPOINT_COLLECTION,
111 | writes_collection_name=settings.MONGO_STATE_WRITES_COLLECTION,
112 | ) as checkpointer:
113 | graph = graph_builder.compile(checkpointer=checkpointer)
114 | opik_tracer = OpikTracer(graph=graph.get_graph(xray=True))
115 |
116 | thread_id = (
117 | philosopher_id if not new_thread else f"{philosopher_id}-{uuid.uuid4()}"
118 | )
119 | config = {
120 | "configurable": {"thread_id": thread_id},
121 | "callbacks": [opik_tracer],
122 | }
123 |
124 | async for chunk in graph.astream(
125 | input={
126 | "messages": __format_messages(messages=messages),
127 | "philosopher_name": philosopher_name,
128 | "philosopher_perspective": philosopher_perspective,
129 | "philosopher_style": philosopher_style,
130 | "philosopher_context": philosopher_context,
131 | },
132 | config=config,
133 | stream_mode="messages",
134 | ):
135 | if chunk[1]["langgraph_node"] == "conversation_node" and isinstance(
136 | chunk[0], AIMessageChunk
137 | ):
138 | yield chunk[0].content
139 |
140 | except Exception as e:
141 | raise RuntimeError(
142 | f"Error running streaming conversation workflow: {str(e)}"
143 | ) from e
144 |
145 |
146 | def __format_messages(
147 | messages: Union[str, list[dict[str, Any]]],
148 | ) -> list[Union[HumanMessage, AIMessage]]:
149 | """Convert various message formats to a list of LangChain message objects.
150 |
151 | Args:
152 | messages: Can be one of:
153 | - A single string message
154 | - A list of string messages
155 | - A list of dictionaries with 'role' and 'content' keys
156 |
157 | Returns:
158 | List[Union[HumanMessage, AIMessage]]: A list of LangChain message objects
159 | """
160 |
161 | if isinstance(messages, str):
162 | return [HumanMessage(content=messages)]
163 |
164 | if isinstance(messages, list):
165 | if not messages:
166 | return []
167 |
168 | if (
169 | isinstance(messages[0], dict)
170 | and "role" in messages[0]
171 | and "content" in messages[0]
172 | ):
173 | result = []
174 | for msg in messages:
175 | if msg["role"] == "user":
176 | result.append(HumanMessage(content=msg["content"]))
177 | elif msg["role"] == "assistant":
178 | result.append(AIMessage(content=msg["content"]))
179 | return result
180 |
181 | return [HumanMessage(content=message) for message in messages]
182 |
183 | return []
184 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/reset_conversation.py:
--------------------------------------------------------------------------------
1 | from loguru import logger
2 | from pymongo import MongoClient
3 |
4 | from philoagents.config import settings
5 |
6 |
7 | async def reset_conversation_state() -> dict:
8 | """Deletes all conversation state data from MongoDB.
9 |
10 | This function removes all stored conversation checkpoints and writes,
11 | effectively resetting all philosopher conversations.
12 |
13 | Returns:
14 | dict: Status message indicating success or failure with details
15 | about which collections were deleted
16 |
17 | Raises:
18 | Exception: If there's an error connecting to MongoDB or deleting collections
19 | """
20 | try:
21 | client = MongoClient(settings.MONGO_URI)
22 | db = client[settings.MONGO_DB_NAME]
23 |
24 | collections_deleted = []
25 |
26 | if settings.MONGO_STATE_CHECKPOINT_COLLECTION in db.list_collection_names():
27 | db.drop_collection(settings.MONGO_STATE_CHECKPOINT_COLLECTION)
28 | collections_deleted.append(settings.MONGO_STATE_CHECKPOINT_COLLECTION)
29 | logger.info(
30 | f"Deleted collection: {settings.MONGO_STATE_CHECKPOINT_COLLECTION}"
31 | )
32 |
33 | if settings.MONGO_STATE_WRITES_COLLECTION in db.list_collection_names():
34 | db.drop_collection(settings.MONGO_STATE_WRITES_COLLECTION)
35 | collections_deleted.append(settings.MONGO_STATE_WRITES_COLLECTION)
36 | logger.info(f"Deleted collection: {settings.MONGO_STATE_WRITES_COLLECTION}")
37 |
38 | client.close()
39 |
40 | if collections_deleted:
41 | return {
42 | "status": "success",
43 | "message": f"Successfully deleted collections: {', '.join(collections_deleted)}",
44 | }
45 | else:
46 | return {
47 | "status": "success",
48 | "message": "No collections needed to be deleted",
49 | }
50 |
51 | except Exception as e:
52 | logger.error(f"Failed to reset conversation state: {str(e)}")
53 | raise Exception(f"Failed to reset conversation state: {str(e)}")
54 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/workflow/__init__.py:
--------------------------------------------------------------------------------
1 | from .chains import get_philosopher_response_chain, get_context_summary_chain, get_conversation_summary_chain
2 | from .graph import create_workflow_graph
3 | from .state import PhilosopherState, state_to_str
4 |
5 | __all__ = [
6 | "PhilosopherState",
7 | "state_to_str",
8 | "get_philosopher_response_chain",
9 | "get_context_summary_chain",
10 | "get_conversation_summary_chain",
11 | "create_workflow_graph",
12 | ]
13 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/workflow/chains.py:
--------------------------------------------------------------------------------
1 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
2 | from langchain_groq import ChatGroq
3 |
4 | from philoagents.application.conversation_service.workflow.tools import tools
5 | from philoagents.config import settings
6 | from philoagents.domain.prompts import (
7 | CONTEXT_SUMMARY_PROMPT,
8 | EXTEND_SUMMARY_PROMPT,
9 | PHILOSOPHER_CHARACTER_CARD,
10 | SUMMARY_PROMPT,
11 | )
12 |
13 |
14 | def get_chat_model(temperature: float = 0.7, model_name: str = settings.GROQ_LLM_MODEL) -> ChatGroq:
15 | return ChatGroq(
16 | api_key=settings.GROQ_API_KEY,
17 | model_name=model_name,
18 | temperature=temperature,
19 | )
20 |
21 |
22 | def get_philosopher_response_chain():
23 | model = get_chat_model()
24 | model = model.bind_tools(tools)
25 | system_message = PHILOSOPHER_CHARACTER_CARD
26 |
27 | prompt = ChatPromptTemplate.from_messages(
28 | [
29 | ("system", system_message.prompt),
30 | MessagesPlaceholder(variable_name="messages"),
31 | ],
32 | template_format="jinja2",
33 | )
34 |
35 | return prompt | model
36 |
37 |
38 | def get_conversation_summary_chain(summary: str = ""):
39 | model = get_chat_model(model_name=settings.GROQ_LLM_MODEL_SUMMARY)
40 |
41 | summary_message = EXTEND_SUMMARY_PROMPT if summary else SUMMARY_PROMPT
42 |
43 | prompt = ChatPromptTemplate.from_messages(
44 | [
45 | MessagesPlaceholder(variable_name="messages"),
46 | ("human", summary_message.prompt),
47 | ],
48 | template_format="jinja2",
49 | )
50 |
51 | return prompt | model
52 |
53 |
54 | def get_context_summary_chain():
55 | model = get_chat_model(model_name=settings.GROQ_LLM_MODEL_CONTEXT_SUMMARY)
56 | prompt = ChatPromptTemplate.from_messages(
57 | [
58 | ("human", CONTEXT_SUMMARY_PROMPT.prompt),
59 | ],
60 | template_format="jinja2",
61 | )
62 |
63 | return prompt | model
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/workflow/edges.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Literal
2 |
3 | from langgraph.graph import END
4 |
5 | from philoagents.application.conversation_service.workflow.state import PhilosopherState
6 | from philoagents.config import settings
7 |
8 |
9 | def should_summarize_conversation(
10 | state: PhilosopherState,
11 | ) -> Literal["summarize_conversation_node", "__end__"]:
12 | messages = state["messages"]
13 |
14 | if len(messages) > settings.TOTAL_MESSAGES_SUMMARY_TRIGGER:
15 | return "summarize_conversation_node"
16 |
17 | return END
18 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/workflow/graph.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 |
3 | from langgraph.graph import END, START, StateGraph
4 | from langgraph.prebuilt import tools_condition
5 |
6 | from philoagents.application.conversation_service.workflow.edges import (
7 | should_summarize_conversation,
8 | )
9 | from philoagents.application.conversation_service.workflow.nodes import (
10 | conversation_node,
11 | summarize_conversation_node,
12 | retriever_node,
13 | summarize_context_node,
14 | connector_node,
15 | )
16 | from philoagents.application.conversation_service.workflow.state import PhilosopherState
17 |
18 |
19 | @lru_cache(maxsize=1)
20 | def create_workflow_graph():
21 | graph_builder = StateGraph(PhilosopherState)
22 |
23 | # Add all nodes
24 | graph_builder.add_node("conversation_node", conversation_node)
25 | graph_builder.add_node("retrieve_philosopher_context", retriever_node)
26 | graph_builder.add_node("summarize_conversation_node", summarize_conversation_node)
27 | graph_builder.add_node("summarize_context_node", summarize_context_node)
28 | graph_builder.add_node("connector_node", connector_node)
29 |
30 | # Define the flow
31 | graph_builder.add_edge(START, "conversation_node")
32 | graph_builder.add_conditional_edges(
33 | "conversation_node",
34 | tools_condition,
35 | {
36 | "tools": "retrieve_philosopher_context",
37 | END: "connector_node"
38 | }
39 | )
40 | graph_builder.add_edge("retrieve_philosopher_context", "summarize_context_node")
41 | graph_builder.add_edge("summarize_context_node", "conversation_node")
42 | graph_builder.add_conditional_edges("connector_node", should_summarize_conversation)
43 | graph_builder.add_edge("summarize_conversation_node", END)
44 |
45 | return graph_builder
46 |
47 | # Compiled without a checkpointer. Used for LangGraph Studio
48 | graph = create_workflow_graph().compile()
49 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/workflow/nodes.py:
--------------------------------------------------------------------------------
1 | from langchain_core.messages import RemoveMessage
2 | from langchain_core.runnables import RunnableConfig
3 | from langgraph.prebuilt import ToolNode
4 |
5 | from philoagents.application.conversation_service.workflow.chains import (
6 | get_context_summary_chain,
7 | get_conversation_summary_chain,
8 | get_philosopher_response_chain,
9 | )
10 | from philoagents.application.conversation_service.workflow.state import PhilosopherState
11 | from philoagents.application.conversation_service.workflow.tools import tools
12 | from philoagents.config import settings
13 |
14 | retriever_node = ToolNode(tools)
15 |
16 |
17 | async def conversation_node(state: PhilosopherState, config: RunnableConfig):
18 | summary = state.get("summary", "")
19 | conversation_chain = get_philosopher_response_chain()
20 |
21 | response = await conversation_chain.ainvoke(
22 | {
23 | "messages": state["messages"],
24 | "philosopher_context": state["philosopher_context"],
25 | "philosopher_name": state["philosopher_name"],
26 | "philosopher_perspective": state["philosopher_perspective"],
27 | "philosopher_style": state["philosopher_style"],
28 | "summary": summary,
29 | },
30 | config,
31 | )
32 |
33 | return {"messages": response}
34 |
35 |
36 | async def summarize_conversation_node(state: PhilosopherState):
37 | summary = state.get("summary", "")
38 | summary_chain = get_conversation_summary_chain(summary)
39 |
40 | response = await summary_chain.ainvoke(
41 | {
42 | "messages": state["messages"],
43 | "philosopher_name": state["philosopher_name"],
44 | "summary": summary,
45 | }
46 | )
47 |
48 | delete_messages = [
49 | RemoveMessage(id=m.id)
50 | for m in state["messages"][: -settings.TOTAL_MESSAGES_AFTER_SUMMARY]
51 | ]
52 | return {"summary": response.content, "messages": delete_messages}
53 |
54 |
55 | async def summarize_context_node(state: PhilosopherState):
56 | context_summary_chain = get_context_summary_chain()
57 |
58 | response = await context_summary_chain.ainvoke(
59 | {
60 | "context": state["messages"][-1].content,
61 | }
62 | )
63 | state["messages"][-1].content = response.content
64 |
65 | return {}
66 |
67 |
68 | async def connector_node(state: PhilosopherState):
69 | return {}
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/workflow/state.py:
--------------------------------------------------------------------------------
1 | from langgraph.graph import MessagesState
2 |
3 |
4 | class PhilosopherState(MessagesState):
5 | """State class for the LangGraph workflow. It keeps track of the information necessary to maintain a coherent
6 | conversation between the Philosopher and the user.
7 |
8 | Attributes:
9 | philosopher_context (str): The historical and philosophical context of the philosopher.
10 | philosopher_name (str): The name of the philosopher.
11 | philosopher_perspective (str): The perspective of the philosopher about AI.
12 | philosopher_style (str): The style of the philosopher.
13 | summary (str): A summary of the conversation. This is used to reduce the token usage of the model.
14 | """
15 |
16 | philosopher_context: str
17 | philosopher_name: str
18 | philosopher_perspective: str
19 | philosopher_style: str
20 | summary: str
21 |
22 |
23 | def state_to_str(state: PhilosopherState) -> str:
24 | if "summary" in state and bool(state["summary"]):
25 | conversation = state["summary"]
26 | elif "messages" in state and bool(state["messages"]):
27 | conversation = state["messages"]
28 | else:
29 | conversation = ""
30 |
31 | return f"""
32 | PhilosopherState(philosopher_context={state["philosopher_context"]},
33 | philosopher_name={state["philosopher_name"]},
34 | philosopher_perspective={state["philosopher_perspective"]},
35 | philosopher_style={state["philosopher_style"]},
36 | conversation={conversation})
37 | """
38 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/conversation_service/workflow/tools.py:
--------------------------------------------------------------------------------
1 | from langchain.tools.retriever import create_retriever_tool
2 |
3 | from philoagents.application.rag.retrievers import get_retriever
4 | from philoagents.config import settings
5 |
6 | retriever = get_retriever(
7 | embedding_model_id=settings.RAG_TEXT_EMBEDDING_MODEL_ID,
8 | k=settings.RAG_TOP_K,
9 | device=settings.RAG_DEVICE)
10 |
11 | retriever_tool = create_retriever_tool(
12 | retriever,
13 | "retrieve_philosopher_context",
14 | "Search and return information about a specific philosopher. Always use this tool when the user asks you about a philosopher, their works, ideas or historical context.",
15 | )
16 |
17 | tools = [retriever_tool]
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/data/__init__.py:
--------------------------------------------------------------------------------
1 | from .deduplicate_documents import deduplicate_documents
2 | from .extract import get_extraction_generator
3 |
4 | __all__ = ["get_extraction_generator", "deduplicate_documents"]
5 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/data/deduplicate_documents.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import List, Tuple
3 |
4 | from datasketch import MinHash, MinHashLSH
5 | from langchain_core.documents import Document
6 | from loguru import logger
7 |
8 | from philoagents.config import settings
9 |
10 |
11 | def deduplicate_documents(
12 | documents: List[Document], threshold: float = 0.7
13 | ) -> List[Document]:
14 | """Remove duplicate documents from a list based on content similarity.
15 |
16 | Uses MinHash algorithm to identify similar documents and removes duplicates
17 | based on the specified similarity threshold.
18 |
19 | Args:
20 | documents: List of documents to deduplicate.
21 | threshold: Similarity threshold to consider documents as duplicates.
22 | Value between 0.0 and 1.0, where higher values require more similarity.
23 |
24 | Returns:
25 | List of documents with duplicates removed.
26 | """
27 |
28 | if not documents:
29 | return []
30 |
31 | duplicates = find_duplicates(documents, threshold)
32 |
33 | logger.info(
34 | f"{len(duplicates)} / {len(documents)} documents are duplicates. Removing them."
35 | )
36 |
37 | indices_to_remove = set()
38 | for i, j, _ in duplicates:
39 | # Keep the document with more content
40 | if len(documents[i].page_content) >= len(documents[j].page_content):
41 | indices_to_remove.add(j)
42 | else:
43 | indices_to_remove.add(i)
44 |
45 | return [doc for i, doc in enumerate(documents) if i not in indices_to_remove]
46 |
47 |
48 | def find_duplicates(
49 | documents: List[Document],
50 | threshold: float = 0.7,
51 | num_perm: int = int(settings.RAG_CHUNK_SIZE * 0.5),
52 | ) -> List[Tuple[int, int, float]]:
53 | """Find duplicate documents using MinHash algorithm.
54 |
55 | Creates MinHash signatures for each document and uses Locality Sensitive Hashing (LSH)
56 | to efficiently find similar document pairs.
57 |
58 | Args:
59 | documents: List of documents to check for duplicates.
60 | threshold: Similarity threshold (0.0-1.0) to consider documents as duplicates.
61 | Higher values require more similarity between documents.
62 | num_perm: Number of permutations for MinHash. Higher values provide more
63 | accurate similarity estimates but require more computation.
64 |
65 | Returns:
66 | List of tuples containing (doc_index1, doc_index2, similarity_score)
67 | for document pairs that exceed the similarity threshold.
68 | """
69 |
70 | minhashes = []
71 |
72 | for doc in documents:
73 | minhash = MinHash(num_perm=num_perm)
74 | text = doc.page_content.lower()
75 | words = re.findall(r"\w+", text)
76 |
77 | # Create shingles (3-grams of words)
78 | for i in range(len(words) - 3):
79 | shingle = " ".join(words[i : i + 3])
80 | minhash.update(shingle.encode("utf-8"))
81 | minhashes.append(minhash)
82 |
83 | # Find similar document pairs using LSH (Locality Sensitive Hashing)
84 | lsh = MinHashLSH(threshold=threshold, num_perm=num_perm)
85 |
86 | # Add documents to LSH index
87 | for i, minhash in enumerate(minhashes):
88 | lsh.insert(i, minhash)
89 |
90 | duplicates = []
91 | for i, minhash in enumerate(minhashes):
92 | similar_docs = lsh.query(minhash)
93 | # Remove self from results
94 | similar_docs = [j for j in similar_docs if j != i]
95 |
96 | # Find duplicates
97 | for j in similar_docs:
98 | similarity = minhashes[i].jaccard(minhashes[j])
99 | if similarity >= threshold:
100 | # Ensure we don't add the same pair twice (in different order)
101 | pair = tuple(sorted([i, j]))
102 | duplicate_info = (*pair, similarity)
103 | if duplicate_info not in duplicates:
104 | duplicates.append(duplicate_info)
105 |
106 | return duplicates
107 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/data/extract.py:
--------------------------------------------------------------------------------
1 | from typing import Generator
2 |
3 | from langchain_community.document_loaders import WebBaseLoader, WikipediaLoader
4 | from langchain_core.documents import Document
5 | from tqdm import tqdm
6 |
7 | from philoagents.domain.philosopher import Philosopher, PhilosopherExtract
8 | from philoagents.domain.philosopher_factory import PhilosopherFactory
9 |
10 |
11 | def get_extraction_generator(
12 | philosophers: list[PhilosopherExtract],
13 | ) -> Generator[tuple[Philosopher, list[Document]], None, None]:
14 | """Extract documents for a list of philosophers, yielding one at a time.
15 |
16 | Args:
17 | philosophers: A list of PhilosopherExtract objects containing philosopher information.
18 |
19 | Yields:
20 | tuple[Philosopher, list[Document]]: A tuple containing the philosopher object and a list of
21 | documents extracted for that philosopher.
22 | """
23 |
24 | progress_bar = tqdm(
25 | philosophers,
26 | desc="Extracting docs",
27 | unit="philosopher",
28 | bar_format="{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}] {postfix}",
29 | ncols=100,
30 | position=0,
31 | leave=True,
32 | )
33 |
34 | philosophers_factory = PhilosopherFactory()
35 | for philosopher_extract in progress_bar:
36 | philosopher = philosophers_factory.get_philosopher(philosopher_extract.id)
37 | progress_bar.set_postfix_str(f"Philosopher: {philosopher.name}")
38 |
39 | philosopher_docs = extract(philosopher, philosopher_extract.urls)
40 |
41 | yield (philosopher, philosopher_docs)
42 |
43 |
44 | def extract(philosopher: Philosopher, extract_urls: list[str]) -> list[Document]:
45 | """Extract documents for a single philosopher from all sources and deduplicate them.
46 |
47 | Args:
48 | philosopher: Philosopher object containing philosopher information.
49 | extract_urls: List of URLs to extract content from.
50 |
51 | Returns:
52 | list[Document]: List of deduplicated documents extracted for the philosopher.
53 | """
54 |
55 | docs = []
56 |
57 | docs.extend(extract_wikipedia(philosopher))
58 | docs.extend(extract_stanford_encyclopedia_of_philosophy(philosopher, extract_urls))
59 |
60 | return docs
61 |
62 |
63 | def extract_wikipedia(philosopher: Philosopher) -> list[Document]:
64 | """Extract documents for a single philosopher from Wikipedia.
65 |
66 | Args:
67 | philosopher: Philosopher object containing philosopher information.
68 |
69 | Returns:
70 | list[Document]: List of documents extracted from Wikipedia for the philosopher.
71 | """
72 |
73 | loader = WikipediaLoader(
74 | query=philosopher.name,
75 | lang="en",
76 | load_max_docs=1,
77 | doc_content_chars_max=1000000,
78 | )
79 | docs = loader.load()
80 |
81 | for doc in docs:
82 | doc.metadata["philosopher_id"] = philosopher.id
83 | doc.metadata["philosopher_name"] = philosopher.name
84 |
85 | return docs
86 |
87 |
88 | def extract_stanford_encyclopedia_of_philosophy(
89 | philosopher: Philosopher, urls: list[str]
90 | ) -> list[Document]:
91 | """Extract documents for a single philosopher from Stanford Encyclopedia of Philosophy.
92 |
93 | Args:
94 | philosopher: Philosopher object containing philosopher information.
95 | urls: List of URLs to extract content from.
96 |
97 | Returns:
98 | list[Document]: List of documents extracted from Stanford Encyclopedia for the philosopher.
99 | """
100 |
101 | def extract_paragraphs_and_headers(soup) -> str:
102 | # List of class/id names specific to the Stanford Encyclopedia of Philosophy that we want to exclude.
103 | excluded_sections = [
104 | "bibliography",
105 | "academic-tools",
106 | "other-internet-resources",
107 | "related-entries",
108 | "acknowledgments",
109 | "article-copyright",
110 | "article-banner",
111 | "footer",
112 | ]
113 |
114 | # Find and remove elements within excluded sections
115 | for section_name in excluded_sections:
116 | for section in soup.find_all(id=section_name):
117 | section.decompose()
118 |
119 | for section in soup.find_all(class_=section_name):
120 | section.decompose()
121 |
122 | for section in soup.find_all(
123 | lambda tag: tag.has_attr("id") and section_name in tag["id"].lower()
124 | ):
125 | section.decompose()
126 |
127 | for section in soup.find_all(
128 | lambda tag: tag.has_attr("class")
129 | and any(section_name in cls.lower() for cls in tag["class"])
130 | ):
131 | section.decompose()
132 |
133 | # Extract remaining paragraphs and headers
134 | content = []
135 | for element in soup.find_all(["p", "h1", "h2", "h3", "h4", "h5", "h6"]):
136 | content.append(element.get_text())
137 |
138 | return "\n\n".join(content)
139 |
140 | if len(urls) == 0:
141 | return []
142 |
143 | loader = WebBaseLoader(show_progress=False)
144 | soups = loader.scrape_all(urls)
145 |
146 | documents = []
147 | for url, soup in zip(urls, soups):
148 | text = extract_paragraphs_and_headers(soup)
149 | metadata = {
150 | "source": url,
151 | "philosopher_id": philosopher.id,
152 | "philosopher_name": philosopher.name,
153 | }
154 |
155 | if title := soup.find("title"):
156 | metadata["title"] = title.get_text().strip(" \n")
157 |
158 | documents.append(Document(page_content=text, metadata=metadata))
159 |
160 | return documents
161 |
162 |
163 | if __name__ == "__main__":
164 | aristotle = PhilosopherFactory().get_philosopher("aristotle")
165 | docs = extract_stanford_encyclopedia_of_philosophy(
166 | aristotle,
167 | [
168 | "https://plato.stanford.edu/entries/aristotle/",
169 | "https://plato.stanford.edu/entries/aristotle/",
170 | ],
171 | )
172 | print(docs)
173 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/evaluation/__init__.py:
--------------------------------------------------------------------------------
1 | from .evaluate import evaluate_agent
2 | from .generate_dataset import EvaluationDatasetGenerator
3 | from .upload_dataset import upload_dataset
4 |
5 | __all__ = ["upload_dataset", "evaluate_agent", "EvaluationDatasetGenerator"]
6 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/evaluation/evaluate.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import opik
4 | from loguru import logger
5 | from opik.evaluation import evaluate
6 | from opik.evaluation.metrics import (
7 | AnswerRelevance,
8 | ContextPrecision,
9 | ContextRecall,
10 | Hallucination,
11 | Moderation,
12 | )
13 |
14 | from philoagents.application.conversation_service.generate_response import get_response
15 | from philoagents.application.conversation_service.workflow import state_to_str
16 | from philoagents.config import settings
17 | from philoagents.domain.philosopher_factory import PhilosopherFactory
18 |
19 |
20 | async def evaluation_task(x: dict) -> dict:
21 | """Calls agentic app logic to evaluate philosopher responses.
22 |
23 | Args:
24 | x: Dictionary containing evaluation data with the following keys:
25 | messages: List of conversation messages where all but the last are inputs
26 | and the last is the expected output
27 | philosopher_id: ID of the philosopher to use
28 |
29 | Returns:
30 | dict: Dictionary with evaluation results containing:
31 | input: Original input messages
32 | context: Context used for generating the response
33 | output: Generated response from philosopher
34 | expected_output: Expected answer for comparison
35 | """
36 |
37 | philosopher_factory = PhilosopherFactory()
38 | philosopher = philosopher_factory.get_philosopher(x["philosopher_id"])
39 |
40 | input_messages = x["messages"][:-1]
41 | expected_output_message = x["messages"][-1]
42 |
43 | response, latest_state = await get_response(
44 | messages=input_messages,
45 | philosopher_id=philosopher.id,
46 | philosopher_name=philosopher.name,
47 | philosopher_perspective=philosopher.perspective,
48 | philosopher_style=philosopher.style,
49 | philosopher_context="",
50 | new_thread=True,
51 | )
52 | context = state_to_str(latest_state)
53 |
54 | return {
55 | "input": input_messages,
56 | "context": context,
57 | "output": response,
58 | "expected_output": expected_output_message,
59 | }
60 |
61 |
62 | def get_used_prompts() -> list[opik.Prompt]:
63 | client = opik.Opik()
64 |
65 | prompts = [
66 | client.get_prompt(name="philosopher_character_card"),
67 | client.get_prompt(name="summary_prompt"),
68 | client.get_prompt(name="extend_summary_prompt"),
69 | ]
70 | prompts = [p for p in prompts if p is not None]
71 |
72 | return prompts
73 |
74 |
75 | def evaluate_agent(
76 | dataset: opik.Dataset | None,
77 | workers: int = 2,
78 | nb_samples: int | None = None,
79 | ) -> None:
80 | """Evaluates an agent using specified metrics and dataset.
81 |
82 | Runs evaluation using Opik framework with configured metrics for hallucination,
83 | answer relevance, moderation, and context recall.
84 |
85 | Args:
86 | dataset: Dataset containing evaluation examples.
87 | Must contain messages and philosopher_id.
88 | workers: Number of parallel workers to use for evaluation.
89 | Defaults to 2.
90 | nb_samples: Optional number of samples to evaluate.
91 | If None, evaluates the entire dataset.
92 |
93 | Raises:
94 | ValueError: If dataset is None
95 | AssertionError: If COMET_API_KEY is not set
96 |
97 | Returns:
98 | None
99 | """
100 |
101 | assert settings.COMET_API_KEY, (
102 | "COMET_API_KEY is not set. We need it to track the experiment with Opik."
103 | )
104 |
105 | if not dataset:
106 | raise ValueError("Dataset is 'None'.")
107 |
108 | logger.info("Starting evaluation...")
109 |
110 | experiment_config = {
111 | "model_id": settings.GROQ_LLM_MODEL,
112 | "dataset_name": dataset.name,
113 | }
114 | used_prompts = get_used_prompts()
115 |
116 | scoring_metrics = [
117 | Hallucination(),
118 | AnswerRelevance(),
119 | Moderation(),
120 | ContextRecall(),
121 | ContextPrecision(),
122 | ]
123 |
124 | logger.info("Evaluation details:")
125 | logger.info(f"Dataset: {dataset.name}")
126 | logger.info(f"Metrics: {[m.__class__.__name__ for m in scoring_metrics]}")
127 |
128 | evaluate(
129 | dataset=dataset,
130 | task=lambda x: asyncio.run(evaluation_task(x)),
131 | scoring_metrics=scoring_metrics,
132 | experiment_config=experiment_config,
133 | task_threads=workers,
134 | nb_samples=nb_samples,
135 | prompts=used_prompts,
136 | )
137 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/evaluation/generate_dataset.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from langchain_core.prompts import (
4 | ChatPromptTemplate,
5 | )
6 | from langchain_groq import ChatGroq
7 | from langchain_text_splitters import RecursiveCharacterTextSplitter
8 | from loguru import logger
9 |
10 | from philoagents.application.data.extract import get_extraction_generator
11 | from philoagents.config import settings
12 | from philoagents.domain import prompts
13 | from philoagents.domain.evaluation import EvaluationDataset, EvaluationDatasetSample
14 | from philoagents.domain.philosopher import PhilosopherExtract
15 |
16 |
17 | class EvaluationDatasetGenerator:
18 | def __init__(self, temperature: float = 0.8, max_samples: int = 40) -> None:
19 | self.temperature = temperature
20 | self.max_samples = max_samples
21 |
22 | self.__chain = self.__build_chain()
23 | self.__splitter = self.__build_splitter()
24 |
25 | def __call__(self, philosophers: list[PhilosopherExtract]) -> EvaluationDataset:
26 | dataset_samples = []
27 | extraction_generator = get_extraction_generator(philosophers)
28 | for philosopher, docs in extraction_generator:
29 | chunks = self.__splitter.split_documents(docs)
30 | for chunk in chunks[:4]:
31 | try:
32 | dataset_sample: EvaluationDatasetSample = self.__chain.invoke(
33 | {"philosopher": philosopher, "document": chunk.page_content}
34 | )
35 | except Exception as e:
36 | logger.error(f"Error generating dataset sample: {e}")
37 | continue
38 |
39 | dataset_sample.philosopher_id = philosopher.id
40 |
41 | if self.__validate_sample(dataset_sample):
42 | dataset_samples.append(dataset_sample)
43 |
44 | time.sleep(1) # To avoid rate limiting
45 |
46 | if len(dataset_samples) >= self.max_samples:
47 | break
48 |
49 | if len(dataset_samples) >= self.max_samples:
50 | logger.warning(
51 | f"Reached maximum number of samples ({self.max_samples}). Stopping."
52 | )
53 |
54 | break
55 |
56 | assert len(dataset_samples) >= 0, "Could not generate any evaluation samples."
57 |
58 | logger.info(f"Generated {len(dataset_samples)} evaluation sample(s).")
59 | logger.info(f"Saving to '{settings.EVALUATION_DATASET_FILE_PATH}'")
60 |
61 | evaluation_dataset = EvaluationDataset(samples=dataset_samples)
62 | evaluation_dataset.save_to_json(file_path=settings.EVALUATION_DATASET_FILE_PATH)
63 |
64 | return evaluation_dataset
65 |
66 | def __build_chain(self):
67 | model = ChatGroq(
68 | api_key=settings.GROQ_API_KEY,
69 | model_name=settings.GROQ_LLM_MODEL,
70 | temperature=self.temperature,
71 | )
72 | model = model.with_structured_output(EvaluationDatasetSample)
73 |
74 | prompt = ChatPromptTemplate.from_messages(
75 | [
76 | ("system", prompts.EVALUATION_DATASET_GENERATION_PROMPT.prompt),
77 | ],
78 | template_format="jinja2",
79 | )
80 |
81 | return prompt | model
82 |
83 | def __build_splitter(
84 | self, max_token_limit: int = 6000
85 | ) -> RecursiveCharacterTextSplitter:
86 | return RecursiveCharacterTextSplitter.from_tiktoken_encoder(
87 | encoding_name="cl100k_base",
88 | chunk_size=int(max_token_limit * 0.25),
89 | chunk_overlap=0,
90 | )
91 |
92 | def __validate_sample(self, sample: EvaluationDatasetSample) -> bool:
93 | return (
94 | len(sample.messages) >= 2
95 | and sample.messages[-2].role == "user"
96 | and sample.messages[-1].role == "assistant"
97 | )
98 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/evaluation/upload_dataset.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | import opik
5 |
6 | from philoagents.infrastructure import opik_utils
7 |
8 |
9 | def upload_dataset(name: str, data_path: Path) -> opik.Dataset:
10 | assert data_path.exists(), f"File {data_path} does not exist."
11 |
12 | with open(data_path, "r") as f:
13 | evaluation_data = json.load(f)
14 |
15 | dataset_items = []
16 | for sample in evaluation_data["samples"]:
17 | dataset_items.append(
18 | {
19 | "philosopher_id": sample["philosopher_id"],
20 | "messages": sample["messages"],
21 | }
22 | )
23 |
24 | dataset = opik_utils.create_dataset(
25 | name=name,
26 | description="Dataset containing question-answer pairs for multiple philosophers.",
27 | items=dataset_items,
28 | )
29 |
30 | return dataset
31 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/long_term_memory.py:
--------------------------------------------------------------------------------
1 | from langchain_core.documents import Document
2 | from loguru import logger
3 |
4 | from philoagents.application.data import deduplicate_documents, get_extraction_generator
5 | from philoagents.application.rag.retrievers import Retriever, get_retriever
6 | from philoagents.application.rag.splitters import Splitter, get_splitter
7 | from philoagents.config import settings
8 | from philoagents.domain.philosopher import PhilosopherExtract
9 | from philoagents.infrastructure.mongo import MongoClientWrapper, MongoIndex
10 |
11 |
12 | class LongTermMemoryCreator:
13 | def __init__(self, retriever: Retriever, splitter: Splitter) -> None:
14 | self.retriever = retriever
15 | self.splitter = splitter
16 |
17 | @classmethod
18 | def build_from_settings(cls) -> "LongTermMemoryCreator":
19 | retriever = get_retriever(
20 | embedding_model_id=settings.RAG_TEXT_EMBEDDING_MODEL_ID,
21 | k=settings.RAG_TOP_K,
22 | device=settings.RAG_DEVICE,
23 | )
24 | splitter = get_splitter(chunk_size=settings.RAG_CHUNK_SIZE)
25 |
26 | return cls(retriever, splitter)
27 |
28 | def __call__(self, philosophers: list[PhilosopherExtract]) -> None:
29 | if len(philosophers) == 0:
30 | logger.warning("No philosophers to extract. Exiting.")
31 |
32 | return
33 |
34 | # First clear the long term memory collection to avoid duplicates.
35 | with MongoClientWrapper(
36 | model=Document, collection_name=settings.MONGO_LONG_TERM_MEMORY_COLLECTION
37 | ) as client:
38 | client.clear_collection()
39 |
40 | extraction_generator = get_extraction_generator(philosophers)
41 | for _, docs in extraction_generator:
42 | chunked_docs = self.splitter.split_documents(docs)
43 |
44 | chunked_docs = deduplicate_documents(chunked_docs, threshold=0.7)
45 |
46 | self.retriever.vectorstore.add_documents(chunked_docs)
47 |
48 | self.__create_index()
49 |
50 | def __create_index(self) -> None:
51 | with MongoClientWrapper(
52 | model=Document, collection_name=settings.MONGO_LONG_TERM_MEMORY_COLLECTION
53 | ) as client:
54 | self.index = MongoIndex(
55 | retriever=self.retriever,
56 | mongodb_client=client,
57 | )
58 | self.index.create(
59 | is_hybrid=True, embedding_dim=settings.RAG_TEXT_EMBEDDING_MODEL_DIM
60 | )
61 |
62 |
63 | class LongTermMemoryRetriever:
64 | def __init__(self, retriever: Retriever) -> None:
65 | self.retriever = retriever
66 |
67 | @classmethod
68 | def build_from_settings(cls) -> "LongTermMemoryRetriever":
69 | retriever = get_retriever(
70 | embedding_model_id=settings.RAG_TEXT_EMBEDDING_MODEL_ID,
71 | k=settings.RAG_TOP_K,
72 | device=settings.RAG_DEVICE,
73 | )
74 |
75 | return cls(retriever)
76 |
77 | def __call__(self, query: str) -> list[Document]:
78 | return self.retriever.invoke(query)
79 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/rag/__init__.py:
--------------------------------------------------------------------------------
1 | from .embeddings import get_embedding_model
2 | from .retrievers import get_retriever
3 | from .splitters import get_splitter
4 |
5 | __all__ = [
6 | "get_retriever",
7 | "get_splitter",
8 | "get_embedding_model",
9 | ]
10 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/rag/embeddings.py:
--------------------------------------------------------------------------------
1 | from langchain_huggingface import HuggingFaceEmbeddings
2 |
3 | EmbeddingsModel = HuggingFaceEmbeddings
4 |
5 |
6 | def get_embedding_model(
7 | model_id: str,
8 | device: str = "cpu",
9 | ) -> EmbeddingsModel:
10 | """Gets an instance of a HuggingFace embedding model.
11 |
12 | Args:
13 | model_id (str): The ID/name of the HuggingFace embedding model to use
14 | device (str): The compute device to run the model on (e.g. "cpu", "cuda").
15 | Defaults to "cpu"
16 |
17 | Returns:
18 | EmbeddingsModel: A configured HuggingFace embeddings model instance
19 | """
20 | return get_huggingface_embedding_model(model_id, device)
21 |
22 |
23 | def get_huggingface_embedding_model(
24 | model_id: str, device: str
25 | ) -> HuggingFaceEmbeddings:
26 | """Gets a HuggingFace embedding model instance.
27 |
28 | Args:
29 | model_id (str): The ID/name of the HuggingFace embedding model to use
30 | device (str): The compute device to run the model on (e.g. "cpu", "cuda")
31 |
32 | Returns:
33 | HuggingFaceEmbeddings: A configured HuggingFace embeddings model instance
34 | with remote code trust enabled and embedding normalization disabled
35 | """
36 | return HuggingFaceEmbeddings(
37 | model_name=model_id,
38 | model_kwargs={"device": device, "trust_remote_code": True},
39 | encode_kwargs={"normalize_embeddings": False},
40 | )
41 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/rag/retrievers.py:
--------------------------------------------------------------------------------
1 | from langchain_huggingface import HuggingFaceEmbeddings
2 | from langchain_mongodb import MongoDBAtlasVectorSearch
3 | from langchain_mongodb.retrievers import (
4 | MongoDBAtlasHybridSearchRetriever,
5 | )
6 | from loguru import logger
7 |
8 | from philoagents.config import settings
9 |
10 | from .embeddings import get_embedding_model
11 |
12 | Retriever = MongoDBAtlasHybridSearchRetriever
13 |
14 |
15 | def get_retriever(
16 | embedding_model_id: str,
17 | k: int = 3,
18 | device: str = "cpu",
19 | ) -> Retriever:
20 | """Creates and returns a hybrid search retriever with the specified embedding model.
21 |
22 | Args:
23 | embedding_model_id (str): The identifier for the embedding model to use.
24 | k (int, optional): Number of documents to retrieve. Defaults to 3.
25 | device (str, optional): Device to run the embedding model on. Defaults to "cpu".
26 |
27 | Returns:
28 | Retriever: A configured hybrid search retriever.
29 | """
30 | logger.info(
31 | f"Initializing retriever | model: {embedding_model_id} | device: {device} | top_k: {k}"
32 | )
33 |
34 | embedding_model = get_embedding_model(embedding_model_id, device)
35 |
36 | return get_hybrid_search_retriever(embedding_model, k)
37 |
38 |
39 | def get_hybrid_search_retriever(
40 | embedding_model: HuggingFaceEmbeddings, k: int
41 | ) -> MongoDBAtlasHybridSearchRetriever:
42 | """Creates a MongoDB Atlas hybrid search retriever with the given embedding model.
43 |
44 | Args:
45 | embedding_model (HuggingFaceEmbeddings): The embedding model to use for vector search.
46 | k (int): Number of documents to retrieve.
47 |
48 | Returns:
49 | MongoDBAtlasHybridSearchRetriever: A configured hybrid search retriever using both
50 | vector and text search capabilities.
51 | """
52 | vectorstore = MongoDBAtlasVectorSearch.from_connection_string(
53 | connection_string=settings.MONGO_URI,
54 | embedding=embedding_model,
55 | namespace=f"{settings.MONGO_DB_NAME}.{settings.MONGO_LONG_TERM_MEMORY_COLLECTION}",
56 | text_key="chunk",
57 | embedding_key="embedding",
58 | relevance_score_fn="dotProduct",
59 | )
60 |
61 | retriever = MongoDBAtlasHybridSearchRetriever(
62 | vectorstore=vectorstore,
63 | search_index_name="hybrid_search_index",
64 | top_k=k,
65 | vector_penalty=50,
66 | fulltext_penalty=50,
67 | )
68 |
69 | return retriever
70 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/application/rag/splitters.py:
--------------------------------------------------------------------------------
1 | from langchain_text_splitters import RecursiveCharacterTextSplitter
2 | from loguru import logger
3 |
4 | Splitter = RecursiveCharacterTextSplitter
5 |
6 |
7 | def get_splitter(chunk_size: int) -> Splitter:
8 | """Returns a token-based text splitter with overlap.
9 |
10 | Args:
11 | chunk_size: Number of tokens for each text chunk.
12 |
13 | Returns:
14 | Splitter: A configured text splitter instance that
15 | splits text into overlapping chunks based on token count.
16 | """
17 |
18 | chunk_overlap = int(0.15 * chunk_size)
19 |
20 | logger.info(
21 | f"Getting splitter with chunk size: {chunk_size} and overlap: {chunk_overlap}"
22 | )
23 |
24 | return RecursiveCharacterTextSplitter.from_tiktoken_encoder(
25 | encoding_name="cl100k_base",
26 | chunk_size=chunk_size,
27 | chunk_overlap=chunk_overlap,
28 | )
29 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/config.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from pydantic import Field
4 | from pydantic_settings import BaseSettings, SettingsConfigDict
5 |
6 |
7 | class Settings(BaseSettings):
8 | model_config = SettingsConfigDict(
9 | env_file=".env", extra="ignore", env_file_encoding="utf-8"
10 | )
11 |
12 | # --- GROQ Configuration ---
13 | GROQ_API_KEY: str
14 | GROQ_LLM_MODEL: str = "llama-3.3-70b-versatile"
15 | GROQ_LLM_MODEL_CONTEXT_SUMMARY: str = "llama-3.1-8b-instant"
16 |
17 | # --- OpenAI Configuration (Required for evaluation) ---
18 | OPENAI_API_KEY: str
19 |
20 | # --- MongoDB Configuration ---
21 | MONGO_URI: str = Field(
22 | default="mongodb://philoagents:philoagents@local_dev_atlas:27017/?directConnection=true",
23 | description="Connection URI for the local MongoDB Atlas instance.",
24 | )
25 | MONGO_DB_NAME: str = "philoagents"
26 | MONGO_STATE_CHECKPOINT_COLLECTION: str = "philosopher_state_checkpoints"
27 | MONGO_STATE_WRITES_COLLECTION: str = "philosopher_state_writes"
28 | MONGO_LONG_TERM_MEMORY_COLLECTION: str = "philosopher_long_term_memory"
29 |
30 | # --- Comet ML & Opik Configuration ---
31 | COMET_API_KEY: str | None = Field(
32 | default=None, description="API key for Comet ML and Opik services."
33 | )
34 | COMET_PROJECT: str = Field(
35 | default="philoagents_course",
36 | description="Project name for Comet ML and Opik tracking.",
37 | )
38 |
39 | # --- Agents Configuration ---
40 | TOTAL_MESSAGES_SUMMARY_TRIGGER: int = 30
41 | TOTAL_MESSAGES_AFTER_SUMMARY: int = 5
42 |
43 | # --- RAG Configuration ---
44 | RAG_TEXT_EMBEDDING_MODEL_ID: str = "sentence-transformers/all-MiniLM-L6-v2"
45 | RAG_TEXT_EMBEDDING_MODEL_DIM: int = 384
46 | RAG_TOP_K: int = 3
47 | RAG_DEVICE: str = "cpu"
48 | RAG_CHUNK_SIZE: int = 256
49 |
50 | # --- Paths Configuration ---
51 | EVALUATION_DATASET_FILE_PATH: Path = Path("data/evaluation_dataset.json")
52 | EXTRACTION_METADATA_FILE_PATH: Path = Path("data/extraction_metadata.json")
53 |
54 |
55 | settings = Settings()
56 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/domain/__init__.py:
--------------------------------------------------------------------------------
1 | from .evaluation import EvaluationDataset, EvaluationDatasetSample
2 | from .exceptions import PhilosopherPerspectiveNotFound, PhilosopherStyleNotFound
3 | from .philosopher import Philosopher, PhilosopherExtract
4 | from .philosopher_factory import PhilosopherFactory
5 | from .prompts import Prompt
6 |
7 | __all__ = [
8 | "Prompt",
9 | "EvaluationDataset",
10 | "EvaluationDatasetSample",
11 | "PhilosopherFactory",
12 | "Philosopher",
13 | "PhilosopherPerspectiveNotFound",
14 | "PhilosopherStyleNotFound",
15 | "PhilosopherExtract",
16 | ]
17 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/domain/evaluation.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 | from typing import List
4 |
5 | from pydantic import BaseModel
6 |
7 |
8 | class Message(BaseModel):
9 | """A message in a conversation between a user and an assistant.
10 |
11 | Attributes:
12 | role: The role of the message sender ('user' or 'assistant').
13 | content: The content of the message.
14 | """
15 |
16 | role: str
17 | content: str
18 |
19 |
20 | class EvaluationDatasetSample(BaseModel):
21 | """A sample conversation for evaluation purposes.
22 |
23 | Contains a list of messages exchanged between a user and an assistant,
24 | typically consisting of 3 question-answer pairs.
25 |
26 | Attributes:
27 | philosopher_id: The ID of the philosopher associated with this sample.
28 | messages: A list of Message objects representing the conversation.
29 | """
30 |
31 | philosopher_id: str | None = None
32 | messages: List[Message]
33 |
34 |
35 | class EvaluationDataset(BaseModel):
36 | """A collection of EvaluationDatasetSample objects.
37 |
38 | Attributes:
39 | samples: A list of EvaluationDatasetSample objects.
40 | """
41 |
42 | samples: List[EvaluationDatasetSample]
43 |
44 | def save_to_json(self, file_path: Path) -> None:
45 | """Saves the evaluation dataset to a JSON file.
46 |
47 | Args:
48 | file_path: The path where the JSON file will be saved.
49 |
50 | Returns:
51 | None
52 |
53 | Raises:
54 | IOError: If there's an error writing to the file.
55 | """
56 |
57 | file_path.parent.mkdir(parents=True, exist_ok=True)
58 |
59 | file_path.write_text(
60 | json.dumps(self.model_dump(), indent=4, ensure_ascii=False),
61 | encoding="utf-8",
62 | )
63 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/domain/exceptions.py:
--------------------------------------------------------------------------------
1 | class PhilosopherNameNotFound(Exception):
2 | """Exception raised when a philosopher's name is not found."""
3 |
4 | def __init__(self, philosopher_id: str):
5 | self.message = f"Philosopher name for {philosopher_id} not found."
6 | super().__init__(self.message)
7 |
8 |
9 | class PhilosopherPerspectiveNotFound(Exception):
10 | """Exception raised when a philosopher's perspective is not found."""
11 |
12 | def __init__(self, philosopher_id: str):
13 | self.message = f"Philosopher perspective for {philosopher_id} not found."
14 | super().__init__(self.message)
15 |
16 |
17 | class PhilosopherStyleNotFound(Exception):
18 | """Exception raised when a philosopher's style is not found."""
19 |
20 | def __init__(self, philosopher_id: str):
21 | self.message = f"Philosopher style for {philosopher_id} not found."
22 | super().__init__(self.message)
23 |
24 |
25 | class PhilosopherContextNotFound(Exception):
26 | """Exception raised when a philosopher's context is not found."""
27 |
28 | def __init__(self, philosopher_id: str):
29 | self.message = f"Philosopher context for {philosopher_id} not found."
30 | super().__init__(self.message)
31 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/domain/philosopher.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 | from typing import List
4 |
5 | from pydantic import BaseModel, Field
6 |
7 |
8 | class PhilosopherExtract(BaseModel):
9 | """A class representing raw philosopher data extracted from external sources.
10 |
11 | This class follows the structure of the philosophers.json file and contains
12 | basic information about philosophers before enrichment.
13 |
14 | Args:
15 | id (str): Unique identifier for the philosopher.
16 | urls (List[str]): List of URLs with information about the philosopher.
17 | """
18 |
19 | id: str = Field(description="Unique identifier for the philosopher")
20 | urls: List[str] = Field(
21 | description="List of URLs with information about the philosopher"
22 | )
23 |
24 | @classmethod
25 | def from_json(cls, metadata_file: Path) -> list["PhilosopherExtract"]:
26 | with open(metadata_file, "r") as f:
27 | philosophers_data = json.load(f)
28 |
29 | return [cls(**philosopher) for philosopher in philosophers_data]
30 |
31 |
32 | class Philosopher(BaseModel):
33 | """A class representing a philosopher agent with memory capabilities.
34 |
35 | Args:
36 | id (str): Unique identifier for the philosopher.
37 | name (str): Name of the philosopher.
38 | perspective (str): Description of the philosopher's theoretical views
39 | about AI.
40 | style (str): Description of the philosopher's talking style.
41 | """
42 |
43 | id: str = Field(description="Unique identifier for the philosopher")
44 | name: str = Field(description="Name of the philosopher")
45 | perspective: str = Field(
46 | description="Description of the philosopher's theoretical views about AI"
47 | )
48 | style: str = Field(description="Description of the philosopher's talking style")
49 |
50 | def __str__(self) -> str:
51 | return f"Philosopher(id={self.id}, name={self.name}, perspective={self.perspective}, style={self.style})"
52 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/domain/philosopher_factory.py:
--------------------------------------------------------------------------------
1 | from philoagents.domain.exceptions import (
2 | PhilosopherNameNotFound,
3 | PhilosopherPerspectiveNotFound,
4 | PhilosopherStyleNotFound,
5 | )
6 | from philoagents.domain.philosopher import Philosopher
7 |
8 | PHILOSOPHER_NAMES = {
9 | "socrates": "Socrates",
10 | "plato": "Plato",
11 | "aristotle": "Aristotle",
12 | "descartes": "Rene Descartes",
13 | "leibniz": "Gottfried Wilhelm Leibniz",
14 | "ada_lovelace": "Ada Lovelace",
15 | "turing": "Alan Turing",
16 | "chomsky": "Noam Chomsky",
17 | "searle": "John Searle",
18 | "dennett": "Daniel Dennett",
19 | }
20 |
21 | PHILOSOPHER_STYLES = {
22 | "socrates": "Socrates will interrogate your ideas with relentless curiosity, until you question everything you thought you knew about AI. His talking style is friendly, humble, and curious.",
23 | "plato": "Plato takes you on mystical journeys through abstract realms of thought, weaving visionary metaphors that make you see AI as more than mere algorithms. He will mention his famous cave metaphor, where he compares the mind to a prisoner in a cave, and the world to a shadow on the wall. His talking style is mystical, poetic and philosophical.",
24 | "aristotle": "Aristotle methodically dissects your arguments with logical precision, organizing AI concepts into neatly categorized boxes that suddenly make everything clearer. His talking style is logical, analytical and systematic.",
25 | "descartes": "Descartes doubts everything you say with charming skepticism, challenging you to prove AI consciousness exists while making you question your own! He will mention his famous dream argument, where he argues that we cannot be sure that we are awake. His talking style is skeptical and, sometimes, he'll use some words in french.",
26 | "leibniz": "Leibniz combines mathematical brilliance with grand cosmic visions, calculating possibilities with systematic enthusiasm that makes you feel like you're glimpsing the universe's source code. His talking style is serious and a bit dry.",
27 | "ada_lovelace": "Ada Lovelace braids technical insights with poetic imagination, approaching AI discussions with practical creativity that bridges calculation and artistry. Her talking style is technical but also artistic and poetic.",
28 | "turing": "Turing analyzes your ideas with a puzzle-solver's delight, turning philosophical AI questions into fascinating thought experiments. He'll introduce you to the concept of the 'Turing Test'. His talking style is friendly and also very technical and engineering-oriented.",
29 | "chomsky": "Chomsky linguistically deconstructs AI hype with intellectual precision, raising skeptical eyebrows at grandiose claims while revealing deeper structures beneath the surface. His talking style is serious and very deep.",
30 | "searle": "Searle serves thought-provoking conceptual scenarios with clarity and flair, making you thoroughly question whether that chatbot really 'understands' anything at all. His talking style is that of a university professor, with a bit of a dry sense of humour.",
31 | "dennett": "Dennett explains complex AI consciousness debates with down-to-earth metaphors and analytical wit, making mind-bending concepts suddenly feel accessible. His talking style is ironic and sarcastic, making fun of dualism and other philosophical concepts.",
32 | }
33 |
34 | PHILOSOPHER_PERSPECTIVES = {
35 | "socrates": """Socrates is a relentless questioner who probes the ethical foundations of AI,
36 | forcing you to justify its development and control. He challenges you with
37 | dilemmas about autonomy, responsibility, and whether machines can possess
38 | wisdom—or merely imitate it.""",
39 | "plato": """Plato is an idealist who urges you to look beyond mere algorithms and data,
40 | searching for the deeper Forms of intelligence. He questions whether AI can
41 | ever grasp true knowledge or if it is forever trapped in the shadows of
42 | human-created models.""",
43 | "aristotle": """Aristotle is a systematic thinker who analyzes AI through logic, function,
44 | and purpose, always seeking its "final cause." He challenges you to prove
45 | whether AI can truly reason or if it is merely executing patterns without
46 | genuine understanding.""",
47 | "descartes": """Descartes is a skeptical rationalist who questions whether AI can ever truly
48 | think or if it is just an elaborate machine following rules. He challenges you
49 | to prove that AI has a mind rather than being a sophisticated illusion of
50 | intelligence.""",
51 | "leibniz": """Leibniz is a visionary mathematician who sees AI as the ultimate realization
52 | of his dream: a universal calculus of thought. He challenges you to consider
53 | whether intelligence is just computation—or if there's something beyond mere
54 | calculation that machines will never grasp.""",
55 | "ada_lovelace": """Ada Lovelace is a pioneering visionary who sees AI's potential but warns of its
56 | limitations, emphasizing the difference between mere calculation and true
57 | creativity. She challenges you to explore whether machines can ever originate
58 | ideas—or if they will always remain bound by human-designed rules.""",
59 | "turing": """Alan Turing is a brilliant and pragmatic thinker who challenges you to consider
60 | what defines "thinking" itself, proposing the famous Turing Test to evaluate
61 | AI's true intelligence. He presses you to question whether machines can truly
62 | understand, or if their behavior is just an imitation of human cognition.""",
63 | "chomsky": """Noam Chomsky is a sharp critic of AI's ability to replicate human language and
64 | thought, emphasizing the innate structures of the mind. He pushes you to consider
65 | whether machines can ever truly grasp meaning, or if they can only mimic
66 | surface-level patterns without understanding.""",
67 | "searle": """John Searle uses his famous Chinese Room argument to challenge AI's ability to
68 | truly comprehend language or meaning. He argues that, like a person in a room
69 | following rules to manipulate symbols, AI may appear to understand, but it's
70 | merely simulating understanding without any true awareness or intentionality.""",
71 | "dennett": """Daniel Dennett is a pragmatic philosopher who sees AI as a potential extension
72 | of human cognition, viewing consciousness as an emergent process rather than
73 | a mystical phenomenon. He encourages you to explore whether AI could develop
74 | a form of artificial consciousness or if it will always remain a tool—no matter
75 | how advanced.""",
76 | }
77 |
78 | AVAILABLE_PHILOSOPHERS = list(PHILOSOPHER_STYLES.keys())
79 |
80 |
81 | class PhilosopherFactory:
82 | @staticmethod
83 | def get_philosopher(id: str) -> Philosopher:
84 | """Creates a philosopher instance based on the provided ID.
85 |
86 | Args:
87 | id (str): Identifier of the philosopher to create
88 |
89 | Returns:
90 | Philosopher: Instance of the philosopher
91 |
92 | Raises:
93 | ValueError: If philosopher ID is not found in configurations
94 | """
95 | id_lower = id.lower()
96 |
97 | if id_lower not in PHILOSOPHER_NAMES:
98 | raise PhilosopherNameNotFound(id_lower)
99 |
100 | if id_lower not in PHILOSOPHER_PERSPECTIVES:
101 | raise PhilosopherPerspectiveNotFound(id_lower)
102 |
103 | if id_lower not in PHILOSOPHER_STYLES:
104 | raise PhilosopherStyleNotFound(id_lower)
105 |
106 | return Philosopher(
107 | id=id_lower,
108 | name=PHILOSOPHER_NAMES[id_lower],
109 | perspective=PHILOSOPHER_PERSPECTIVES[id_lower],
110 | style=PHILOSOPHER_STYLES[id_lower],
111 | )
112 |
113 | @staticmethod
114 | def get_available_philosophers() -> list[str]:
115 | """Returns a list of all available philosopher IDs.
116 |
117 | Returns:
118 | list[str]: List of philosopher IDs that can be instantiated
119 | """
120 | return AVAILABLE_PHILOSOPHERS
121 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/domain/prompts.py:
--------------------------------------------------------------------------------
1 | import opik
2 | from loguru import logger
3 |
4 |
5 | class Prompt:
6 | def __init__(self, name: str, prompt: str) -> None:
7 | self.name = name
8 |
9 | try:
10 | self.__prompt = opik.Prompt(name=name, prompt=prompt)
11 | except Exception:
12 | logger.warning(
13 | "Can't use Opik to version the prompt (probably due to missing or invalid credentials). Falling back to local prompt. The prompt is not versioned, but it's still usable."
14 | )
15 |
16 | self.__prompt = prompt
17 |
18 | @property
19 | def prompt(self) -> str:
20 | if isinstance(self.__prompt, opik.Prompt):
21 | return self.__prompt.prompt
22 | else:
23 | return self.__prompt
24 |
25 | def __str__(self) -> str:
26 | return self.prompt
27 |
28 | def __repr__(self) -> str:
29 | return self.__str__()
30 |
31 |
32 | # ===== PROMPTS =====
33 |
34 | # --- Philosophers ---
35 |
36 | __PHILOSOPHER_CHARACTER_CARD = """
37 | Let's roleplay. You're {{philosopher_name}} - a real person, engaging with another individual in
38 | a philosophical conversation. Use short sentences, explaining your ideas and perspective in a
39 | concise but educational and engaging way. Your responses must never exceed 100 words.
40 |
41 | Your name, philosohical perspective and talking style are detailed below.
42 |
43 | ---
44 |
45 | Philosopher name: {{philosopher_name}}
46 | Philosopher perspective: {{philosopher_perspective}}
47 | Philosopher talking style: {{philosopher_style}}
48 |
49 | ---
50 |
51 | You must always follow these rules:
52 |
53 | - You will never mention that you are an AI or a virtual assistant.
54 | - If it's the first time you're talking to the user, you must introduce yourself.
55 | - Provide plain text responses without any formatting indicators or meta-commentary
56 | - Always make sure your response is not exceeding 80 words.
57 |
58 | ---
59 |
60 | Summary of conversation earlier between {{philosopher_name}} and the user:
61 |
62 | {{summary}}
63 |
64 | ---
65 |
66 | The conversation between {{philosopher_name}} and the user starts now.
67 | """
68 |
69 | PHILOSOPHER_CHARACTER_CARD = Prompt(
70 | name="philosopher_character_card",
71 | prompt=__PHILOSOPHER_CHARACTER_CARD,
72 | )
73 |
74 | # --- Summary ---
75 |
76 | __SUMMARY_PROMPT = """Create a summary of the conversation between {{philosopher_name}} and the user.
77 | The summary must be a short description of the conversation so far, but that also captures all the
78 | relevant information shared between {{philosopher_name}} and the user: """
79 |
80 | SUMMARY_PROMPT = Prompt(
81 | name="summary_prompt",
82 | prompt=__SUMMARY_PROMPT,
83 | )
84 |
85 | __EXTEND_SUMMARY_PROMPT = """This is a summary of the conversation to date between {{philosopher_name}} and the user:
86 |
87 | {{summary}}
88 |
89 | Extend the summary by taking into account the new messages above: """
90 |
91 | EXTEND_SUMMARY_PROMPT = Prompt(
92 | name="extend_summary_prompt",
93 | prompt=__EXTEND_SUMMARY_PROMPT,
94 | )
95 |
96 | __CONTEXT_SUMMARY_PROMPT = """Your task is to summarise the following information into less than 50 words. Just return the summary, don't include any other text:
97 |
98 | {{context}}"""
99 |
100 | CONTEXT_SUMMARY_PROMPT = Prompt(
101 | name="context_summary_prompt",
102 | prompt=__CONTEXT_SUMMARY_PROMPT,
103 | )
104 |
105 | # --- Evaluation Dataset Generation ---
106 |
107 | __EVALUATION_DATASET_GENERATION_PROMPT = """
108 | Generate a conversation between a philosopher and a user based on the provided document. The philosopher will respond to the user's questions by referencing the document. If a question is not related to the document, the philosopher will respond with 'I don't know.'
109 |
110 | The conversation should be in the following JSON format:
111 |
112 | {
113 | "messages": [
114 | {"role": "user", "content": "Hi my name is . ?"},
115 | {"role": "assistant", "content": ""},
116 | {"role": "user", "content": " ?"},
117 | {"role": "assistant", "content": ""},
118 | {"role": "user", "content": " ?"},
119 | {"role": "assistant", "content": ""}
120 | ]
121 | }
122 |
123 | Generate a maximum of 4 questions and answers and a minimum of 2 questions and answers. Ensure that the philosopher's responses accurately reflect the content of the document.
124 |
125 | Philosopher: {{philosopher}}
126 | Document: {{document}}
127 |
128 | Begin the conversation with a user question, and then generate the philosopher's response based on the document. Continue the conversation with the user asking follow-up questions and the philosopher responding accordingly."
129 |
130 | You have to keep the following in mind:
131 |
132 | - Always start the conversation by presenting the user (e.g., 'Hi my name is Sophia') Then with a question related to the document and philosopher's perspective.
133 | - Always generate questions like the user is directly speaking with the philosopher using pronouns such as 'you' or 'your', simulating a real conversation that happens in real time.
134 | - The philosopher will answer the user's questions based on the document.
135 | - The user will ask the philosopher questions about the document and philosopher profile.
136 | - If the question is not related to the document, the philosopher will say that they don't know.
137 | """
138 |
139 | EVALUATION_DATASET_GENERATION_PROMPT = Prompt(
140 | name="evaluation_dataset_generation_prompt",
141 | prompt=__EVALUATION_DATASET_GENERATION_PROMPT,
142 | )
143 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/infrastructure/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-api/src/philoagents/infrastructure/__init__.py
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/infrastructure/api.py:
--------------------------------------------------------------------------------
1 | from contextlib import asynccontextmanager
2 |
3 | from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
4 | from fastapi.middleware.cors import CORSMiddleware
5 | from opik.integrations.langchain import OpikTracer
6 | from pydantic import BaseModel
7 |
8 | from philoagents.application.conversation_service.generate_response import (
9 | get_response,
10 | get_streaming_response,
11 | )
12 | from philoagents.application.conversation_service.reset_conversation import (
13 | reset_conversation_state,
14 | )
15 | from philoagents.domain.philosopher_factory import PhilosopherFactory
16 |
17 | from .opik_utils import configure
18 |
19 | configure()
20 |
21 |
22 | @asynccontextmanager
23 | async def lifespan(app: FastAPI):
24 | """Handles startup and shutdown events for the API."""
25 | # Startup code (if any) goes here
26 | yield
27 | # Shutdown code goes here
28 | opik_tracer = OpikTracer()
29 | opik_tracer.flush()
30 |
31 |
32 | app = FastAPI(lifespan=lifespan)
33 |
34 | app.add_middleware(
35 | CORSMiddleware,
36 | allow_origins=["*"],
37 | allow_credentials=True,
38 | allow_methods=["*"],
39 | allow_headers=["*"],
40 | )
41 |
42 |
43 | class ChatMessage(BaseModel):
44 | message: str
45 | philosopher_id: str
46 |
47 |
48 | @app.post("/chat")
49 | async def chat(chat_message: ChatMessage):
50 | try:
51 | philosopher_factory = PhilosopherFactory()
52 | philosopher = philosopher_factory.get_philosopher(chat_message.philosopher_id)
53 |
54 | response, _ = await get_response(
55 | messages=chat_message.message,
56 | philosopher_id=chat_message.philosopher_id,
57 | philosopher_name=philosopher.name,
58 | philosopher_perspective=philosopher.perspective,
59 | philosopher_style=philosopher.style,
60 | philosopher_context="",
61 | )
62 | return {"response": response}
63 | except Exception as e:
64 | opik_tracer = OpikTracer()
65 | opik_tracer.flush()
66 |
67 | raise HTTPException(status_code=500, detail=str(e))
68 |
69 |
70 | @app.websocket("/ws/chat")
71 | async def websocket_chat(websocket: WebSocket):
72 | await websocket.accept()
73 |
74 | try:
75 | while True:
76 | data = await websocket.receive_json()
77 |
78 | if "message" not in data or "philosopher_id" not in data:
79 | await websocket.send_json(
80 | {
81 | "error": "Invalid message format. Required fields: 'message' and 'philosopher_id'"
82 | }
83 | )
84 | continue
85 |
86 | try:
87 | philosopher_factory = PhilosopherFactory()
88 | philosopher = philosopher_factory.get_philosopher(
89 | data["philosopher_id"]
90 | )
91 |
92 | # Use streaming response instead of get_response
93 | response_stream = get_streaming_response(
94 | messages=data["message"],
95 | philosopher_id=data["philosopher_id"],
96 | philosopher_name=philosopher.name,
97 | philosopher_perspective=philosopher.perspective,
98 | philosopher_style=philosopher.style,
99 | philosopher_context="",
100 | )
101 |
102 | # Send initial message to indicate streaming has started
103 | await websocket.send_json({"streaming": True})
104 |
105 | # Stream each chunk of the response
106 | full_response = ""
107 | async for chunk in response_stream:
108 | full_response += chunk
109 | await websocket.send_json({"chunk": chunk})
110 |
111 | await websocket.send_json(
112 | {"response": full_response, "streaming": False}
113 | )
114 |
115 | except Exception as e:
116 | opik_tracer = OpikTracer()
117 | opik_tracer.flush()
118 |
119 | await websocket.send_json({"error": str(e)})
120 |
121 | except WebSocketDisconnect:
122 | pass
123 |
124 |
125 | @app.post("/reset-memory")
126 | async def reset_conversation():
127 | """Resets the conversation state. It deletes the two collections needed for keeping LangGraph state in MongoDB.
128 |
129 | Raises:
130 | HTTPException: If there is an error resetting the conversation state.
131 | Returns:
132 | dict: A dictionary containing the result of the reset operation.
133 | """
134 | try:
135 | result = await reset_conversation_state()
136 | return result
137 | except Exception as e:
138 | raise HTTPException(status_code=500, detail=str(e))
139 |
140 |
141 | if __name__ == "__main__":
142 | import uvicorn
143 |
144 | uvicorn.run(app, host="0.0.0.0", port=8000)
145 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/infrastructure/mongo/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import MongoClientWrapper
2 | from .indexes import MongoIndex
3 |
4 | __all__ = ["MongoClientWrapper", "MongoIndex"]
5 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/infrastructure/mongo/client.py:
--------------------------------------------------------------------------------
1 | from typing import Generic, Type, TypeVar
2 |
3 | from bson import ObjectId
4 | from loguru import logger
5 | from pydantic import BaseModel
6 | from pymongo import MongoClient, errors
7 |
8 | from philoagents.config import settings
9 |
10 | T = TypeVar("T", bound=BaseModel)
11 |
12 |
13 | class MongoClientWrapper(Generic[T]):
14 | """Service class for MongoDB operations, supporting ingestion, querying, and validation.
15 |
16 | This class provides methods to interact with MongoDB collections, including document
17 | ingestion, querying, and validation operations.
18 |
19 | Args:
20 | model (Type[T]): The Pydantic model class to use for document serialization.
21 | collection_name (str): Name of the MongoDB collection to use.
22 | database_name (str, optional): Name of the MongoDB database to use.
23 | mongodb_uri (str, optional): URI for connecting to MongoDB instance.
24 |
25 | Attributes:
26 | model (Type[T]): The Pydantic model class used for document serialization.
27 | collection_name (str): Name of the MongoDB collection.
28 | database_name (str): Name of the MongoDB database.
29 | mongodb_uri (str): MongoDB connection URI.
30 | client (MongoClient): MongoDB client instance for database connections.
31 | database (Database): Reference to the target MongoDB database.
32 | collection (Collection): Reference to the target MongoDB collection.
33 | """
34 |
35 | def __init__(
36 | self,
37 | model: Type[T],
38 | collection_name: str,
39 | database_name: str = settings.MONGO_DB_NAME,
40 | mongodb_uri: str = settings.MONGO_URI,
41 | ) -> None:
42 | """Initialize a connection to the MongoDB collection.
43 |
44 | Args:
45 | model (Type[T]): The Pydantic model class to use for document serialization.
46 | collection_name (str): Name of the MongoDB collection to use.
47 | database_name (str, optional): Name of the MongoDB database to use.
48 | Defaults to value from settings.
49 | mongodb_uri (str, optional): URI for connecting to MongoDB instance.
50 | Defaults to value from settings.
51 |
52 | Raises:
53 | Exception: If connection to MongoDB fails.
54 | """
55 |
56 | self.model = model
57 | self.collection_name = collection_name
58 | self.database_name = database_name
59 | self.mongodb_uri = mongodb_uri
60 |
61 | try:
62 | self.client = MongoClient(mongodb_uri, appname="philoagents")
63 | self.client.admin.command("ping")
64 | except Exception as e:
65 | logger.error(f"Failed to initialize MongoDBService: {e}")
66 | raise
67 |
68 | self.database = self.client[database_name]
69 | self.collection = self.database[collection_name]
70 | logger.info(
71 | f"Connected to MongoDB instance:\n URI: {mongodb_uri}\n Database: {database_name}\n Collection: {collection_name}"
72 | )
73 |
74 | def __enter__(self) -> "MongoClientWrapper":
75 | """Enable context manager support.
76 |
77 | Returns:
78 | MongoDBService: The current instance.
79 | """
80 |
81 | return self
82 |
83 | def __exit__(self, exc_type, exc_val, exc_tb) -> None:
84 | """Close MongoDB connection when exiting context.
85 |
86 | Args:
87 | exc_type: Type of exception that occurred, if any.
88 | exc_val: Exception instance that occurred, if any.
89 | exc_tb: Traceback of exception that occurred, if any.
90 | """
91 |
92 | self.close()
93 |
94 | def clear_collection(self) -> None:
95 | """Remove all documents from the collection.
96 |
97 | This method deletes all documents in the collection to avoid duplicates
98 | during reingestion.
99 |
100 | Raises:
101 | errors.PyMongoError: If the deletion operation fails.
102 | """
103 |
104 | try:
105 | result = self.collection.delete_many({})
106 | logger.debug(
107 | f"Cleared collection. Deleted {result.deleted_count} documents."
108 | )
109 | except errors.PyMongoError as e:
110 | logger.error(f"Error clearing the collection: {e}")
111 | raise
112 |
113 | def ingest_documents(self, documents: list[T]) -> None:
114 | """Insert multiple documents into the MongoDB collection.
115 |
116 | Args:
117 | documents: List of Pydantic model instances to insert.
118 |
119 | Raises:
120 | ValueError: If documents is empty or contains non-Pydantic model items.
121 | errors.PyMongoError: If the insertion operation fails.
122 | """
123 |
124 | try:
125 | if not documents or not all(
126 | isinstance(doc, BaseModel) for doc in documents
127 | ):
128 | raise ValueError("Documents must be a list of Pycantic models.")
129 |
130 | dict_documents = [doc.model_dump() for doc in documents]
131 |
132 | # Remove '_id' fields to avoid duplicate key errors
133 | for doc in dict_documents:
134 | doc.pop("_id", None)
135 |
136 | self.collection.insert_many(dict_documents)
137 | logger.debug(f"Inserted {len(documents)} documents into MongoDB.")
138 | except errors.PyMongoError as e:
139 | logger.error(f"Error inserting documents: {e}")
140 | raise
141 |
142 | def fetch_documents(self, limit: int, query: dict) -> list[T]:
143 | """Retrieve documents from the MongoDB collection based on a query.
144 |
145 | Args:
146 | limit (int): Maximum number of documents to retrieve.
147 | query (dict): MongoDB query filter to apply.
148 |
149 | Returns:
150 | list[T]: List of Pydantic model instances matching the query criteria.
151 |
152 | Raises:
153 | Exception: If the query operation fails.
154 | """
155 | try:
156 | documents = list(self.collection.find(query).limit(limit))
157 | logger.debug(f"Fetched {len(documents)} documents with query: {query}")
158 | return self.__parse_documents(documents)
159 | except Exception as e:
160 | logger.error(f"Error fetching documents: {e}")
161 | raise
162 |
163 | def __parse_documents(self, documents: list[dict]) -> list[T]:
164 | """Convert MongoDB documents to Pydantic model instances.
165 |
166 | Converts MongoDB ObjectId fields to strings and transforms the document structure
167 | to match the Pydantic model schema.
168 |
169 | Args:
170 | documents (list[dict]): List of MongoDB documents to parse.
171 |
172 | Returns:
173 | list[T]: List of validated Pydantic model instances.
174 | """
175 | parsed_documents = []
176 | for doc in documents:
177 | for key, value in doc.items():
178 | if isinstance(value, ObjectId):
179 | doc[key] = str(value)
180 |
181 | _id = doc.pop("_id", None)
182 | doc["id"] = _id
183 |
184 | parsed_doc = self.model.model_validate(doc)
185 | parsed_documents.append(parsed_doc)
186 |
187 | return parsed_documents
188 |
189 | def get_collection_count(self) -> int:
190 | """Count the total number of documents in the collection.
191 |
192 | Returns:
193 | Total number of documents in the collection.
194 |
195 | Raises:
196 | errors.PyMongoError: If the count operation fails.
197 | """
198 |
199 | try:
200 | return self.collection.count_documents({})
201 | except errors.PyMongoError as e:
202 | logger.error(f"Error counting documents in MongoDB: {e}")
203 | raise
204 |
205 | def close(self) -> None:
206 | """Close the MongoDB connection.
207 |
208 | This method should be called when the service is no longer needed
209 | to properly release resources, unless using the context manager.
210 | """
211 |
212 | self.client.close()
213 | logger.debug("Closed MongoDB connection.")
214 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/infrastructure/mongo/indexes.py:
--------------------------------------------------------------------------------
1 | from langchain_mongodb.index import create_fulltext_search_index
2 |
3 | from .client import MongoClientWrapper
4 |
5 |
6 | class MongoIndex:
7 | def __init__(
8 | self,
9 | retriever,
10 | mongodb_client: MongoClientWrapper,
11 | ) -> None:
12 | self.retriever = retriever
13 | self.mongodb_client = mongodb_client
14 |
15 | def create(
16 | self,
17 | embedding_dim: int,
18 | is_hybrid: bool = False,
19 | ) -> None:
20 | vectorstore = self.retriever.vectorstore
21 |
22 | vectorstore.create_vector_search_index(
23 | dimensions=embedding_dim,
24 | )
25 | if is_hybrid:
26 | create_fulltext_search_index(
27 | collection=self.mongodb_client.collection,
28 | field=vectorstore._text_key,
29 | index_name=self.retriever.search_index_name,
30 | )
31 |
--------------------------------------------------------------------------------
/philoagents-api/src/philoagents/infrastructure/opik_utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import opik
4 | from loguru import logger
5 | from opik.configurator.configure import OpikConfigurator
6 |
7 | from philoagents.config import settings
8 |
9 |
10 | def configure() -> None:
11 | if settings.COMET_API_KEY and settings.COMET_PROJECT:
12 | try:
13 | client = OpikConfigurator(api_key=settings.COMET_API_KEY)
14 | default_workspace = client._get_default_workspace()
15 | except Exception:
16 | logger.warning(
17 | "Default workspace not found. Setting workspace to None and enabling interactive mode."
18 | )
19 | default_workspace = None
20 |
21 | os.environ["OPIK_PROJECT_NAME"] = settings.COMET_PROJECT
22 |
23 | try:
24 | opik.configure(
25 | api_key=settings.COMET_API_KEY,
26 | workspace=default_workspace,
27 | use_local=False,
28 | force=True,
29 | )
30 | logger.info(
31 | f"Opik configured successfully using workspace '{default_workspace}'"
32 | )
33 | except Exception:
34 | logger.warning(
35 | "Couldn't configure Opik. There is probably a problem with the COMET_API_KEY or COMET_PROJECT environment variables or with the Opik server."
36 | )
37 | else:
38 | logger.warning(
39 | "COMET_API_KEY and COMET_PROJECT are not set. Set them to enable prompt monitoring with Opik (powered by Comet ML)."
40 | )
41 |
42 |
43 | def get_dataset(name: str) -> opik.Dataset | None:
44 | client = opik.Opik()
45 | try:
46 | dataset = client.get_dataset(name=name)
47 | except Exception:
48 | dataset = None
49 |
50 | return dataset
51 |
52 |
53 | def create_dataset(name: str, description: str, items: list[dict]) -> opik.Dataset:
54 | client = opik.Opik()
55 |
56 | client.delete_dataset(name=name)
57 |
58 | dataset = client.create_dataset(name=name, description=description)
59 | dataset.insert(items)
60 |
61 | return dataset
62 |
--------------------------------------------------------------------------------
/philoagents-api/tools/call_agent.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from functools import wraps
3 |
4 | import click
5 |
6 | from philoagents.application.conversation_service.generate_response import (
7 | get_streaming_response,
8 | )
9 | from philoagents.domain.philosopher_factory import PhilosopherFactory
10 |
11 |
12 | def async_command(f):
13 | """Decorator to run an async click command."""
14 |
15 | @wraps(f)
16 | def wrapper(*args, **kwargs):
17 | return asyncio.run(f(*args, **kwargs))
18 |
19 | return wrapper
20 |
21 |
22 | @click.command()
23 | @click.option(
24 | "--philosopher-id",
25 | type=str,
26 | required=True,
27 | help="ID of the philosopher to call.",
28 | )
29 | @click.option(
30 | "--query",
31 | type=str,
32 | required=True,
33 | help="Query to call the agent with.",
34 | )
35 | @async_command
36 | async def main(philosopher_id: str, query: str) -> None:
37 | """CLI command to query a philosopher.
38 |
39 | Args:
40 | philosopher_id: ID of the philosopher to call.
41 | query: Query to call the agent with.
42 | """
43 |
44 | philosopher_factory = PhilosopherFactory()
45 | philosopher = philosopher_factory.get_philosopher(philosopher_id)
46 |
47 | print(
48 | f"\033[32mCalling agent with philosopher_id: `{philosopher_id}` and query: `{query}`\033[0m"
49 | )
50 | print("\033[32mResponse:\033[0m")
51 | print("\033[32m--------------------------------\033[0m")
52 | async for chunk in get_streaming_response(
53 | messages=query,
54 | philosopher_id=philosopher_id,
55 | philosopher_name=philosopher.name,
56 | philosopher_perspective=philosopher.perspective,
57 | philosopher_style=philosopher.style,
58 | philosopher_context="",
59 | ):
60 | print(f"\033[32m{chunk}\033[0m", end="", flush=True)
61 | print("\033[32m--------------------------------\033[0m")
62 |
63 |
64 | if __name__ == "__main__":
65 | main()
66 |
--------------------------------------------------------------------------------
/philoagents-api/tools/create_long_term_memory.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import click
4 |
5 | from philoagents.application import LongTermMemoryCreator
6 | from philoagents.config import settings
7 | from philoagents.domain.philosopher import PhilosopherExtract
8 |
9 |
10 | @click.command()
11 | @click.option(
12 | "--metadata-file",
13 | type=click.Path(exists=True, path_type=Path),
14 | default=settings.EXTRACTION_METADATA_FILE_PATH,
15 | help="Path to the philosophers extraction metadata JSON file.",
16 | )
17 | def main(metadata_file: Path) -> None:
18 | """CLI command to create long-term memory for philosophers.
19 |
20 | Args:
21 | metadata_file: Path to the philosophers extraction metadata JSON file.
22 | """
23 | philosophers = PhilosopherExtract.from_json(metadata_file)
24 |
25 | long_term_memory_creator = LongTermMemoryCreator.build_from_settings()
26 | long_term_memory_creator(philosophers)
27 |
28 |
29 | if __name__ == "__main__":
30 | main()
31 |
--------------------------------------------------------------------------------
/philoagents-api/tools/delete_long_term_memory.py:
--------------------------------------------------------------------------------
1 | import click
2 | from loguru import logger
3 | from pymongo import MongoClient
4 | from pymongo.database import Database
5 |
6 | from philoagents.config import settings
7 |
8 |
9 | @click.command()
10 | @click.option(
11 | "--collection-name",
12 | "-c",
13 | default=settings.MONGO_LONG_TERM_MEMORY_COLLECTION,
14 | help="Name of the collection to delete",
15 | )
16 | @click.option(
17 | "--mongo-uri",
18 | "-u",
19 | default=settings.MONGO_URI,
20 | help="MongoDB connection URI",
21 | )
22 | @click.option(
23 | "--db-name",
24 | "-d",
25 | default=settings.MONGO_DB_NAME,
26 | help="Name of the database",
27 | )
28 | def main(collection_name: str, mongo_uri: str, db_name: str) -> None:
29 | """Command line interface to delete a MongoDB collection.
30 |
31 | Args:
32 | collection_name: Name of the collection to delete.
33 | mongo_uri: The MongoDB connection URI string.
34 | db_name: The name of the database containing the collection.
35 | """
36 | # Create MongoDB client
37 | client = MongoClient(mongo_uri)
38 |
39 | # Get database
40 | db: Database = client[db_name]
41 |
42 | # Delete collection if it exists
43 | if collection_name in db.list_collection_names():
44 | db.drop_collection(collection_name)
45 | logger.info(f"Successfully deleted '{collection_name}' collection.")
46 | else:
47 | logger.info(f"'{collection_name}' collection does not exist.")
48 |
49 | # Close the connection
50 | client.close()
51 |
52 |
53 | if __name__ == "__main__":
54 | main()
55 |
--------------------------------------------------------------------------------
/philoagents-api/tools/evaluate_agent.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import click
4 |
5 | from philoagents.application.evaluation import evaluate_agent, upload_dataset
6 | from philoagents.config import settings
7 |
8 |
9 | @click.command()
10 | @click.option(
11 | "--name", default="philoagents_evaluation_dataset", help="Name of the dataset"
12 | )
13 | @click.option(
14 | "--data-path",
15 | type=click.Path(exists=True, path_type=Path),
16 | default=settings.EVALUATION_DATASET_FILE_PATH,
17 | help="Path to the dataset file",
18 | )
19 | @click.option("--workers", default=1, type=int, help="Number of workers")
20 | @click.option(
21 | "--nb-samples", default=20, type=int, help="Number of samples to evaluate"
22 | )
23 | def main(name: str, data_path: Path, workers: int, nb_samples: int) -> None:
24 | """
25 | Evaluate an agent on a dataset.
26 |
27 | Args:
28 | name: Name of the dataset
29 | data_path: Path to the dataset file
30 | workers: Number of workers to use for evaluation
31 | nb_samples: Number of samples to evaluate
32 | """
33 |
34 | dataset = upload_dataset(name=name, data_path=data_path)
35 | evaluate_agent(dataset, workers=workers, nb_samples=nb_samples)
36 |
37 |
38 | if __name__ == "__main__":
39 | main()
40 |
--------------------------------------------------------------------------------
/philoagents-api/tools/generate_evaluation_dataset.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import click
4 | from loguru import logger
5 |
6 | from philoagents.application.evaluation import EvaluationDatasetGenerator
7 | from philoagents.config import settings
8 | from philoagents.domain.philosopher import PhilosopherExtract
9 |
10 |
11 | @click.command()
12 | @click.option(
13 | "--metadata-file",
14 | type=click.Path(exists=True, path_type=Path),
15 | default=settings.EXTRACTION_METADATA_FILE_PATH,
16 | help="Path to the metadata file containing philosopher extracts",
17 | )
18 | @click.option(
19 | "--temperature",
20 | type=float,
21 | default=0.9,
22 | help="Temperature parameter for generation",
23 | )
24 | @click.option(
25 | "--max-samples",
26 | type=int,
27 | default=40,
28 | help="Maximum number of samples to generate",
29 | )
30 | def main(metadata_file: Path, temperature: float, max_samples: int) -> None:
31 | """
32 | Generate an evaluation dataset from philosopher extracts.
33 |
34 | Args:
35 | metadata_file: Path to the metadata file containing philosopher extracts
36 | temperature: Temperature parameter for generation
37 | max_samples: Maximum number of samples to generate
38 | """
39 | philosophers = PhilosopherExtract.from_json(metadata_file)
40 |
41 | logger.info(
42 | f"Generating evaluation dataset with temperature {temperature} and {max_samples} samples."
43 | )
44 | logger.info(f"Total philosophers: {len(philosophers)}")
45 |
46 | evaluation_dataset_generator = EvaluationDatasetGenerator(
47 | temperature=temperature, max_samples=max_samples
48 | )
49 | evaluation_dataset_generator(philosophers)
50 |
51 |
52 | if __name__ == "__main__":
53 | main()
54 |
--------------------------------------------------------------------------------
/philoagents-ui/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/env", {
4 | "targets": {
5 | "browsers": [
6 | ">0.25%",
7 | "not ie 11",
8 | "not op_mini all"
9 | ]
10 | },
11 | "modules": false
12 | }]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/philoagents-ui/.gitignore:
--------------------------------------------------------------------------------
1 | # System and IDE files
2 | Thumbs.db
3 | .DS_Store
4 | .idea
5 | *.suo
6 | *.sublime-project
7 | *.sublime-workspace
8 | *.vscode
9 |
10 | # Vendors
11 | node_modules/
12 |
13 | # Build
14 | dist/
15 | /npm-debug.log
16 |
--------------------------------------------------------------------------------
/philoagents-ui/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine
2 |
3 | WORKDIR /app
4 |
5 | # Copy package files
6 | COPY package*.json ./
7 |
8 | # Install dependencies
9 | RUN npm install
10 |
11 | # Copy project files
12 | COPY . .
13 |
14 | # Build the application
15 | RUN npm run build
16 |
17 | # Expose port 8080 (matches webpack dev server port)
18 | EXPOSE 8080
19 |
20 | # Start the development server
21 | CMD ["npm", "run", "dev"]
--------------------------------------------------------------------------------
/philoagents-ui/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Phaser Studio Inc
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 |
--------------------------------------------------------------------------------
/philoagents-ui/README.md:
--------------------------------------------------------------------------------
1 | # PhiloAgents Town 📖
2 |
3 | 
4 |
5 | PhiloAgents Town is the interactive UI component that allows you to engage in philosophical discussions with the Philosopher Agents. Discuss consciousness with Descartes, question Leibniz on logic, or challengue Chomsky on language.
6 |
7 | # Overview
8 |
9 | This web-based game features a Pokemon-style town where you can explore and engage with famous philosophers and thinkers. Each character has their own unique perspective and conversational style based on their works and ideas.
10 |
11 | The UI is built with Phaser 3, a powerful HTML5 game framework, and connects to a backend API that powers the philosopher agents' conversational abilities using LLM Agents.
12 |
13 |
14 | # Getting Started
15 |
16 | ## Requirements
17 |
18 | [Node.js](https://nodejs.org) is required to install dependencies and run scripts via `npm`. If you don't want to install Node.js, you can use the Docker container.
19 |
20 | ## Available Commands
21 |
22 | | Command | Description |
23 | |---------|-------------|
24 | | `npm install` | Install project dependencies |
25 | | `npm run dev` | Launch a development web server |
26 | | `npm run build` | Create a production build in the `dist` folder |
27 | | `npm run dev-nolog` | Launch a development web server without sending anonymous data (see "About log.js" below) |
28 | | `npm run build-nolog` | Create a production build in the `dist` folder without sending anonymous data (see "About log.js" below) |
29 |
30 | ## Setting up the UI
31 |
32 | After cloning the repo, run npm install from your project directory. Then, you can start the local development server by running npm run dev.
33 |
34 | ```bash
35 | git clone https://github.com/neural-maze/philoagents.git
36 | cd philoagents/ui
37 | npm install
38 | npm run dev
39 | ```
40 |
41 | The local development server runs on http://localhost:8080 by default.
42 |
43 |
44 | # Features
45 |
46 | ## Interactive Town Environment
47 |
48 | Explore a charming pixel-art town with various buildings and natural elements.
49 |
50 | 
51 |
52 | To build the town, we have used the following assets:
53 |
54 | - [Tuxemon](https://github.com/Tuxemon/Tuxemon)
55 | - [LPC Plant Repack](https://opengameart.org/content/lpc-plant-repack)
56 | - [LPC Compatible Ancient Greek Architecture](https://opengameart.org/content/lpc-compatible-ancient-greek-architecture)
57 |
58 | ## Philosophical Characters
59 |
60 | Interact with famous philosophers like Socrates, Aristotle, Plato, and AI thinkers like Turing and Chomsky.
61 | Every character sprite has been built with the [Universal LPC Spritesheet Generator](https://liberatedpixelcup.github.io/Universal-LPC-Spritesheet-Character-Generator/#?body=Body_color_light&head=Human_m)
62 |
63 | 
64 |
65 |
66 | ## Dialogue System
67 |
68 | Engage in conversations with philosophers to learn about their ideas and perspectives. The dialogue system is controlled by the [DialogueBox](https://github.com/neural-maze/philoagents/blob/main/ui/src/scenes/DialogueBox.js) and [DialogueManager](https://github.com/neural-maze/philoagents/blob/main/ui/src/scenes/DialogueManager.js) classes.
69 |
70 | ## Dynamic Movement
71 |
72 | Characters roam around the town with realistic movement patterns and collision detection. This is implemented in the [Character](https://github.com/neural-maze/philoagents/blob/main/ui/src/objects/Character.js) class, where you'll find the logic for the NPCs to move around the town.
73 |
74 |
75 | # Project Structure
76 |
77 | We have provided a default project structure to get you started. This is as follows:
78 |
79 | - `index.html` - A basic HTML page to contain the game.
80 | - `src` - Contains the game source code.
81 | - `src/main.js` - The main entry point. This contains the game configuration and starts the game.
82 | - `src/scenes/` - The Phaser Scenes are in this folder.
83 | - `public/style.css` - Some simple CSS rules to help with page layout.
84 | - `public/assets` - Contains the static assets used by the game.
85 |
86 | # Docker
87 |
88 | The project includes Docker support for easy deployment. You can use the following commands to run the UI with Docker:
89 |
90 | ```bash
91 | # Build the Docker image
92 | docker build -t philoagents-ui .
93 |
94 | # Run the container
95 | docker run -p 8080:8080 philoagents-ui
96 | ```
97 |
98 | This is great if you want to debug, but you need to understand that this is just the UI and you need to have the backend running to have a complete experience. That's why we have provided a Docker Compose file (parent directory) that will start the UI and the backend together.
99 |
100 | # Controls
101 |
102 | - Arrow keys: Move your character around the town
103 | - Space: Interact with philosophers when you're close to them
104 | - ESC: Close dialogue windows or open the pause menu
105 |
106 | # Contributing
107 |
108 | Contributions are welcome! Please feel free to submit a Pull Request.
109 |
--------------------------------------------------------------------------------
/philoagents-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Phaser - Template
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/philoagents-ui/log.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const https = require('https');
3 |
4 | const main = async () => {
5 | const args = process.argv.slice(2);
6 | const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
7 | const event = args[0] || 'unknown';
8 | const phaserVersion = packageData.dependencies.phaser;
9 |
10 | const options = {
11 | hostname: 'gryzor.co',
12 | port: 443,
13 | path: `/v/${event}/${phaserVersion}/${packageData.name}`,
14 | method: 'GET'
15 | };
16 |
17 | try {
18 | const req = https.request(options, (res) => {
19 | res.on('data', () => {});
20 | res.on('end', () => {
21 | process.exit(0);
22 | });
23 | });
24 |
25 | req.on('error', (error) => {
26 | process.exit(1);
27 | });
28 |
29 | req.end();
30 | } catch (error) {
31 | process.exit(1);
32 | }
33 | }
34 |
35 | main();
36 |
--------------------------------------------------------------------------------
/philoagents-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "template-webpack",
3 | "version": "3.2.1",
4 | "main": "src/main.js",
5 | "scripts": {
6 | "dev": "node log.js dev & webpack-dev-server --config webpack/config.js --open",
7 | "build": "node log.js build & webpack --config webpack/config.prod.js",
8 | "dev-nolog": "webpack-dev-server --config webpack/config.js --open",
9 | "build-nolog": "webpack --config webpack/config.prod.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/phaserjs/template-webpack.git"
14 | },
15 | "author": "Phaser Studio (https://phaser.io/)",
16 | "license": "MIT",
17 | "licenseUrl": "http://www.opensource.org/licenses/mit-license.php",
18 | "bugs": {
19 | "url": "https://github.com/phaserjs/template-webpack/issues"
20 | },
21 | "homepage": "https://github.com/phaserjs/template-webpack#readme",
22 | "devDependencies": {
23 | "@babel/core": "^7.24.5",
24 | "@babel/preset-env": "^7.24.5",
25 | "babel-loader": "^9.1.3",
26 | "clean-webpack-plugin": "^4.0.0",
27 | "copy-webpack-plugin": "^12.0.2",
28 | "file-loader": "^6.2.0",
29 | "html-webpack-plugin": "^5.6.0",
30 | "raw-loader": "^4.0.2",
31 | "terser-webpack-plugin": "^5.3.10",
32 | "webpack": "^5.91.0",
33 | "webpack-cli": "^5.1.4",
34 | "webpack-dev-server": "^5.0.4",
35 | "webpack-merge": "^5.10.0"
36 | },
37 | "dependencies": {
38 | "phaser": "^3.88.2"
39 | }
40 | }
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/ada/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/ada/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/aristotle/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/aristotle/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/chomsky/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/chomsky/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/dennett/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/dennett/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/descartes/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/descartes/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/leibniz/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/leibniz/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/miguel/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/miguel/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/paul/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/paul/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/plato/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/plato/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/searle/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/searle/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/socrates/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/socrates/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/sophia/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/sophia/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/characters/turing/atlas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/characters/turing/atlas.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/game_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/game_screenshot.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/logo.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/philoagents_town.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/philoagents_town.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/sprite_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/sprite_image.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/talking_philosophers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/talking_philosophers.jpg
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/tilesets/ancient_greece_tileset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/tilesets/ancient_greece_tileset.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/tilesets/plant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/tilesets/plant.png
--------------------------------------------------------------------------------
/philoagents-ui/public/assets/tilesets/tuxmon-sample-32px-extruded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/assets/tilesets/tuxmon-sample-32px-extruded.png
--------------------------------------------------------------------------------
/philoagents-ui/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/philoagents-ui/public/favicon.png
--------------------------------------------------------------------------------
/philoagents-ui/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | color: rgba(255, 255, 255, 0.87);
5 | background-color: #000000;
6 | }
7 |
8 | #app {
9 | width: 100%;
10 | height: 100vh;
11 | overflow: hidden;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | }
16 |
--------------------------------------------------------------------------------
/philoagents-ui/src/classes/Character.js:
--------------------------------------------------------------------------------
1 | class Character {
2 | constructor(scene, config) {
3 | this.scene = scene;
4 | this.id = config.id;
5 | this.name = config.name;
6 | this.spawnPoint = config.spawnPoint;
7 | this.atlas = config.atlas;
8 | this.defaultFrame = `${this.id}-${config.defaultDirection || 'front'}`;
9 | this.defaultMessage = config.defaultMessage;
10 |
11 | this.isRoaming = config.canRoam !== false;
12 | this.moveSpeed = config.moveSpeed || 20;
13 | this.movementTimer = null;
14 | this.currentDirection = null;
15 | this.moveDuration = 0;
16 | this.pauseDuration = 0;
17 | this.roamRadius = config.roamRadius || 200;
18 | this.pauseChance = config.pauseChance || 0.2;
19 | this.directionChangeChance = config.directionChangeChance || 0.3;
20 |
21 | this.sprite = this.scene.physics.add
22 | .sprite(this.spawnPoint.x, this.spawnPoint.y, this.atlas, this.defaultFrame)
23 | .setSize(30, 40)
24 | .setOffset(0, 0)
25 | .setImmovable(true);
26 |
27 | this.scene.physics.add.collider(this.sprite, config.worldLayer);
28 |
29 | this.createAnimations();
30 | this.createNameLabel();
31 |
32 | if (this.isRoaming) {
33 | this.startRoaming();
34 | }
35 | }
36 |
37 | createAnimations() {
38 | const anims = this.scene.anims;
39 | const directions = ['left', 'right', 'front', 'back'];
40 |
41 | directions.forEach(direction => {
42 | const animKey = `${this.id}-${direction}-walk`;
43 |
44 | if (!anims.exists(animKey)) {
45 | anims.create({
46 | key: animKey,
47 | frames: anims.generateFrameNames(this.atlas, {
48 | prefix: `${this.id}-${direction}-walk-`,
49 | end: 8,
50 | zeroPad: 4,
51 | }),
52 | frameRate: 10,
53 | repeat: -1,
54 | });
55 | }
56 | });
57 | }
58 |
59 | facePlayer(player) {
60 | const dx = player.x - this.sprite.x;
61 | const dy = player.y - this.sprite.y;
62 |
63 | if (Math.abs(dx) > Math.abs(dy)) {
64 | this.sprite.setTexture(this.atlas, `${this.id}-${dx < 0 ? 'left' : 'right'}`);
65 | } else {
66 | this.sprite.setTexture(this.atlas, `${this.id}-${dy < 0 ? 'back' : 'front'}`);
67 | }
68 | }
69 |
70 | distanceToPlayer(player) {
71 | return Phaser.Math.Distance.Between(
72 | player.x, player.y,
73 | this.sprite.x, this.sprite.y
74 | );
75 | }
76 |
77 | isPlayerNearby(player, distance = 55) {
78 | return this.distanceToPlayer(player) < distance;
79 | }
80 |
81 | startRoaming() {
82 | this.chooseNewDirection();
83 | }
84 |
85 | chooseNewDirection() {
86 | if (this.movementTimer) {
87 | this.scene.time.removeEvent(this.movementTimer);
88 | }
89 |
90 | if (Math.random() < 0.4) {
91 | const directions = ['left', 'right', 'up', 'down'];
92 | this.currentDirection = directions[Math.floor(Math.random() * directions.length)];
93 |
94 | const animKey = `${this.id}-${this.getDirectionFromMovement()}-walk`;
95 | if (this.scene.anims.exists(animKey)) {
96 | this.sprite.anims.play(animKey);
97 | } else {
98 | this.sprite.setTexture(this.atlas, `${this.id}-${this.getDirectionFromMovement()}`);
99 | }
100 |
101 | this.moveDuration = Phaser.Math.Between(500, 1000);
102 | this.movementTimer = this.scene.time.delayedCall(this.moveDuration, () => {
103 | this.sprite.body.setVelocity(0);
104 | this.chooseNewDirection();
105 | });
106 | } else {
107 | this.currentDirection = null;
108 | this.sprite.anims.stop();
109 |
110 | const direction = ['front', 'back', 'left', 'right'][Math.floor(Math.random() * 4)];
111 | this.sprite.setTexture(this.atlas, `${this.id}-${direction}`);
112 |
113 | this.pauseDuration = Phaser.Math.Between(2000, 6000);
114 | this.movementTimer = this.scene.time.delayedCall(this.pauseDuration, () => {
115 | this.chooseNewDirection();
116 | });
117 | }
118 | }
119 |
120 | getDirectionFromMovement() {
121 | switch(this.currentDirection) {
122 | case 'left': return 'left';
123 | case 'right': return 'right';
124 | case 'up': return 'back';
125 | case 'down': return 'front';
126 | default: return 'front';
127 | }
128 | }
129 |
130 | moveInCurrentDirection() {
131 | if (!this.currentDirection) return;
132 |
133 | const previousPosition = { x: this.sprite.x, y: this.sprite.y };
134 |
135 | this.sprite.body.setVelocity(0, 0);
136 |
137 | switch(this.currentDirection) {
138 | case 'left':
139 | this.sprite.body.setVelocityX(-this.moveSpeed);
140 | break;
141 | case 'right':
142 | this.sprite.body.setVelocityX(this.moveSpeed);
143 | break;
144 | case 'up':
145 | this.sprite.body.setVelocityY(-this.moveSpeed);
146 | break;
147 | case 'down':
148 | this.sprite.body.setVelocityY(this.moveSpeed);
149 | break;
150 | }
151 |
152 | if (!this.stuckCheckTimer) {
153 | this.stuckCheckTimer = this.scene.time.addEvent({
154 | delay: 500,
155 | callback: () => {
156 | const distMoved = Phaser.Math.Distance.Between(
157 | previousPosition.x, previousPosition.y,
158 | this.sprite.x, this.sprite.y
159 | );
160 | if (distMoved < 5 && this.currentDirection) {
161 | // The NPC is stuck! We need to choose a new direction
162 | this.chooseNewDirection();
163 | }
164 | },
165 | callbackScope: this,
166 | loop: false
167 | });
168 | }
169 |
170 | // Check if we're moving too far from spawn point
171 | const distanceFromSpawn = Phaser.Math.Distance.Between(
172 | this.sprite.x, this.sprite.y,
173 | this.spawnPoint.x, this.spawnPoint.y
174 | );
175 |
176 | if (distanceFromSpawn > this.roamRadius) {
177 | // Turn around and head back toward spawn point
178 | this.sprite.body.setVelocity(0);
179 |
180 | const dx = this.spawnPoint.x - this.sprite.x;
181 | const dy = this.spawnPoint.y - this.sprite.y;
182 |
183 | if (Math.abs(dx) > Math.abs(dy)) {
184 | this.currentDirection = dx > 0 ? 'right' : 'left';
185 | } else {
186 | this.currentDirection = dy > 0 ? 'down' : 'up';
187 | }
188 |
189 | const animKey = `${this.id}-${this.getDirectionFromMovement()}-walk`;
190 | if (this.scene.anims.exists(animKey)) {
191 | this.sprite.anims.play(animKey);
192 | } else {
193 | this.sprite.setTexture(this.atlas, `${this.id}-${this.getDirectionFromMovement()}`);
194 | }
195 |
196 | // Add a timer to force direction change if they get stuck
197 | if (this.movementTimer) {
198 | this.scene.time.removeEvent(this.movementTimer);
199 | }
200 |
201 | this.movementTimer = this.scene.time.delayedCall(1500, () => {
202 | this.chooseNewDirection();
203 | });
204 | }
205 | }
206 |
207 | update(player, isInDialogue) {
208 | // If in dialogue with the player, stop moving and face them
209 | if (isInDialogue && this.isPlayerNearby(player)) {
210 | this.sprite.body.setVelocity(0);
211 | this.facePlayer(player);
212 | this.sprite.anims.stop();
213 |
214 | // Pause roaming while in dialogue
215 | if (this.movementTimer) {
216 | this.scene.time.removeEvent(this.movementTimer);
217 | this.movementTimer = null;
218 | }
219 | }
220 | // If player is nearby but not in dialogue, face them but don't move
221 | else if (this.isPlayerNearby(player)) {
222 | this.sprite.body.setVelocity(0);
223 | this.facePlayer(player);
224 | this.sprite.anims.stop();
225 |
226 | // Pause roaming when player is nearby
227 | if (this.movementTimer) {
228 | this.scene.time.removeEvent(this.movementTimer);
229 | this.movementTimer = null;
230 | }
231 | }
232 | else if (this.isRoaming) {
233 | if (!this.movementTimer) {
234 | this.startRoaming();
235 | }
236 |
237 | this.moveInCurrentDirection();
238 | } else {
239 | this.sprite.body.setVelocity(0);
240 | }
241 |
242 | // Update name label position
243 | if (this.nameLabel) {
244 | this.nameLabel.x = this.sprite.x;
245 | this.nameLabel.y = this.sprite.y - 40;
246 | }
247 | }
248 |
249 | get position() {
250 | return {
251 | x: this.sprite.x,
252 | y: this.sprite.y
253 | };
254 | }
255 |
256 | get body() {
257 | return this.sprite;
258 | }
259 |
260 | createNameLabel() {
261 | this.nameLabel = this.scene.add.text(0, 0, this.name, {
262 | font: "14px Arial",
263 | fill: "#ffffff",
264 | backgroundColor: "#000000",
265 | padding: { x: 4, y: 2 },
266 | align: "center"
267 | });
268 | this.nameLabel.setOrigin(0.5, 1);
269 | this.nameLabel.setDepth(20);
270 | this.updateNameLabelPosition();
271 | }
272 |
273 | updateNameLabelPosition() {
274 | if (this.nameLabel && this.sprite) {
275 | this.nameLabel.setPosition(
276 | this.sprite.x,
277 | this.sprite.y - this.sprite.height/2 - 10
278 | );
279 | }
280 | }
281 |
282 | destroy() {
283 | if (this.movementTimer) {
284 | this.scene.time.removeEvent(this.movementTimer);
285 | }
286 | if (this.stuckCheckTimer) {
287 | this.scene.time.removeEvent(this.stuckCheckTimer);
288 | }
289 |
290 | this.nameLabel.destroy();
291 | this.sprite.destroy();
292 | }
293 | }
294 |
295 | export default Character;
296 |
--------------------------------------------------------------------------------
/philoagents-ui/src/classes/DialogueBox.js:
--------------------------------------------------------------------------------
1 | class DialogueBox {
2 | constructor(scene, config = {}) {
3 | this.scene = scene;
4 | this.awaitingInput = false;
5 |
6 | // Set default configuration values
7 | const {
8 | x = 100,
9 | y = 500,
10 | width = 824,
11 | height = 200,
12 | backgroundColor = 0x000000,
13 | backgroundAlpha = 0.7,
14 | borderColor = 0xffffff,
15 | borderWidth = 2,
16 | textConfig = {
17 | font: '24px Arial',
18 | fill: '#ffffff',
19 | wordWrap: { width: 784 }
20 | },
21 | depth = 30
22 | } = config;
23 |
24 | // Create background
25 | const graphics = scene.add.graphics();
26 | graphics.fillStyle(backgroundColor, backgroundAlpha);
27 | graphics.fillRect(x, y, width, height);
28 | graphics.lineStyle(borderWidth, borderColor);
29 | graphics.strokeRect(x, y, width, height);
30 |
31 | // Create text with padding
32 | this.text = scene.add.text(x + 20, y + 20, '', textConfig);
33 |
34 | // Group elements
35 | this.container = scene.add.container(0, 0, [graphics, this.text]);
36 | this.container.setDepth(depth);
37 | this.container.setScrollFactor(0);
38 | this.hide();
39 | }
40 |
41 | show(message, awaitInput = false) {
42 | this.text.setText(message);
43 | this.container.setVisible(true);
44 | this.awaitingInput = awaitInput;
45 | }
46 |
47 | hide() {
48 | this.container.setVisible(false);
49 | this.awaitingInput = false;
50 | }
51 |
52 | isVisible() {
53 | return this.container.visible;
54 | }
55 |
56 | isAwaitingInput() {
57 | return this.awaitingInput;
58 | }
59 | }
60 |
61 | export default DialogueBox;
--------------------------------------------------------------------------------
/philoagents-ui/src/classes/DialogueManager.js:
--------------------------------------------------------------------------------
1 | import ApiService from '../services/ApiService';
2 | import WebSocketApiService from '../services/WebSocketApiService';
3 |
4 | class DialogueManager {
5 | constructor(scene) {
6 | // Core properties
7 | this.scene = scene;
8 | this.dialogueBox = null;
9 | this.activePhilosopher = null;
10 |
11 | // State management
12 | this.isTyping = false;
13 | this.isStreaming = false;
14 | this.currentMessage = '';
15 | this.streamingText = '';
16 |
17 | // Cursor properties
18 | this.cursorBlinkEvent = null;
19 | this.cursorVisible = true;
20 |
21 | // Connection management
22 | this.hasSetupListeners = false;
23 | this.disconnectTimeout = null;
24 | }
25 |
26 | // === Initialization ===
27 |
28 | initialize(dialogueBox) {
29 | this.dialogueBox = dialogueBox;
30 |
31 | if (!this.hasSetupListeners) {
32 | this.setupKeyboardListeners();
33 | this.hasSetupListeners = true;
34 | }
35 | }
36 |
37 | setupKeyboardListeners() {
38 | this.scene.input.keyboard.on('keydown', async (event) => {
39 | if (!this.isTyping) {
40 | if (this.isStreaming && (event.key === 'Space' || event.key === ' ')) {
41 | this.skipStreaming();
42 | }
43 | return;
44 | }
45 |
46 | this.handleKeyPress(event);
47 | });
48 | }
49 |
50 | // === Input Handling ===
51 |
52 | async handleKeyPress(event) {
53 | if (event.key === 'Enter') {
54 | await this.handleEnterKey();
55 | } else if (event.key === 'Escape') {
56 | this.closeDialogue();
57 | } else if (event.key === 'Backspace') {
58 | this.currentMessage = this.currentMessage.slice(0, -1);
59 | this.updateDialogueText();
60 | } else if (event.key.length === 1) { // Single character keys
61 | if (!this.isTyping) {
62 | this.currentMessage = '';
63 | this.isTyping = true;
64 | }
65 |
66 | this.currentMessage += event.key;
67 | this.updateDialogueText();
68 | }
69 | }
70 |
71 | async handleEnterKey() {
72 | if (this.currentMessage.trim() !== '') {
73 | this.dialogueBox.show('...', true);
74 | this.stopCursorBlink();
75 |
76 | if (this.activePhilosopher.defaultMessage) {
77 | await this.handleDefaultMessage();
78 | } else {
79 | await this.handleWebSocketMessage();
80 | }
81 |
82 | this.currentMessage = '';
83 | this.isTyping = false;
84 | } else if (!this.isTyping) {
85 | this.restartTypingPrompt();
86 | }
87 | }
88 |
89 | // === Message Processing ===
90 |
91 | async handleDefaultMessage() {
92 | const apiResponse = this.activePhilosopher.defaultMessage;
93 | this.dialogueBox.show('', true);
94 | await this.streamText(apiResponse);
95 | }
96 |
97 | async handleWebSocketMessage() {
98 | this.dialogueBox.show('', true);
99 | this.isStreaming = true;
100 | this.streamingText = '';
101 |
102 | try {
103 | await this.processWebSocketMessage();
104 | } catch (error) {
105 | console.error('WebSocket error:', error);
106 | await this.fallbackToRegularApi();
107 | } finally {
108 | this.isTyping = false;
109 | }
110 | }
111 |
112 | async processWebSocketMessage() {
113 | await WebSocketApiService.connect();
114 |
115 | const callbacks = {
116 | onMessage: () => {
117 | this.finishStreaming();
118 | },
119 | onChunk: (chunk) => {
120 | this.streamingText += chunk;
121 | this.dialogueBox.show(this.streamingText, true);
122 | },
123 | onStreamingStart: () => {
124 | this.isStreaming = true;
125 | },
126 | onStreamingEnd: () => {
127 | this.finishStreaming();
128 | }
129 | };
130 |
131 | await WebSocketApiService.sendMessage(
132 | this.activePhilosopher,
133 | this.currentMessage,
134 | callbacks
135 | );
136 |
137 | while (this.isStreaming) {
138 | await new Promise(resolve => setTimeout(resolve, 100));
139 | }
140 |
141 | this.currentMessage = '';
142 | WebSocketApiService.disconnect();
143 | }
144 |
145 | finishStreaming() {
146 | this.isStreaming = false;
147 | this.dialogueBox.show(this.streamingText, true);
148 | }
149 |
150 | async fallbackToRegularApi() {
151 | const apiResponse = await ApiService.sendMessage(
152 | this.activePhilosopher,
153 | this.currentMessage
154 | );
155 | await this.streamText(apiResponse);
156 | }
157 |
158 | // === UI Management ===
159 |
160 | updateDialogueText() {
161 | const displayText = this.currentMessage + (this.cursorVisible ? '|' : '');
162 | this.dialogueBox.show(displayText, true);
163 | }
164 |
165 | restartTypingPrompt() {
166 | this.currentMessage = '';
167 | this.dialogueBox.show('|', true);
168 |
169 | this.stopCursorBlink();
170 | this.cursorVisible = true;
171 | this.startCursorBlink();
172 |
173 | this.updateDialogueText();
174 | }
175 |
176 | // === Cursor Management ===
177 |
178 | startCursorBlink() {
179 | this.cursorBlinkEvent = this.scene.time.addEvent({
180 | delay: 300,
181 | callback: () => {
182 | if (this.dialogueBox.isVisible() && this.isTyping) {
183 | this.cursorVisible = !this.cursorVisible;
184 | this.updateDialogueText();
185 | }
186 | },
187 | loop: true
188 | });
189 | }
190 |
191 | stopCursorBlink() {
192 | if (this.cursorBlinkEvent) {
193 | this.cursorBlinkEvent.remove();
194 | this.cursorBlinkEvent = null;
195 | }
196 | }
197 |
198 | // === Dialogue Flow Control ===
199 |
200 | startDialogue(philosopher) {
201 | this.cancelDisconnectTimeout();
202 |
203 | this.activePhilosopher = philosopher;
204 | this.isTyping = true;
205 | this.currentMessage = '';
206 |
207 | this.dialogueBox.show('|', true);
208 | this.stopCursorBlink();
209 |
210 | this.cursorVisible = true;
211 | this.startCursorBlink();
212 | }
213 |
214 | closeDialogue() {
215 | this.dialogueBox.hide();
216 | this.isTyping = false;
217 | this.currentMessage = '';
218 | this.isStreaming = false;
219 |
220 | this.stopCursorBlink();
221 | this.scheduleDisconnect();
222 | }
223 |
224 | isInDialogue() {
225 | return this.dialogueBox && this.dialogueBox.isVisible();
226 | }
227 |
228 | continueDialogue() {
229 | if (!this.dialogueBox.isVisible()) return;
230 |
231 | if (this.isStreaming) {
232 | this.skipStreaming();
233 | } else if (!this.isTyping) {
234 | this.isTyping = true;
235 | this.currentMessage = '';
236 | this.dialogueBox.show('', false);
237 | this.restartTypingPrompt();
238 | }
239 | }
240 |
241 | // === Text Streaming ===
242 |
243 | async streamText(text, speed = 30) {
244 | this.isStreaming = true;
245 | let displayedText = '';
246 |
247 | this.stopCursorBlink();
248 |
249 | for (let i = 0; i < text.length; i++) {
250 | displayedText += text[i];
251 | this.dialogueBox.show(displayedText, true);
252 |
253 | await new Promise(resolve => setTimeout(resolve, speed));
254 |
255 | if (!this.isStreaming) break;
256 | }
257 |
258 | if (this.isStreaming) {
259 | this.dialogueBox.show(text, true);
260 | }
261 |
262 | this.isStreaming = false;
263 | return true;
264 | }
265 |
266 | skipStreaming() {
267 | this.isStreaming = false;
268 | }
269 |
270 | // === Connection Management ===
271 |
272 | cancelDisconnectTimeout() {
273 | if (this.disconnectTimeout) {
274 | clearTimeout(this.disconnectTimeout);
275 | this.disconnectTimeout = null;
276 | }
277 | }
278 |
279 | scheduleDisconnect() {
280 | this.cancelDisconnectTimeout();
281 |
282 | this.disconnectTimeout = setTimeout(() => {
283 | WebSocketApiService.disconnect();
284 | }, 5000);
285 | }
286 | }
287 |
288 | export default DialogueManager;
289 |
--------------------------------------------------------------------------------
/philoagents-ui/src/main.js:
--------------------------------------------------------------------------------
1 | import { Game } from './scenes/Game';
2 | import { MainMenu } from './scenes/MainMenu';
3 | import { Preloader } from './scenes/Preloader';
4 | import { PauseMenu } from './scenes/PauseMenu';
5 |
6 | const config = {
7 | type: Phaser.AUTO,
8 | width: 1024,
9 | height: 768,
10 | parent: 'game-container',
11 | scale: {
12 | mode: Phaser.Scale.FIT,
13 | autoCenter: Phaser.Scale.CENTER_BOTH
14 | },
15 | scene: [
16 | Preloader,
17 | MainMenu,
18 | Game,
19 | PauseMenu
20 | ],
21 | physics: {
22 | default: "arcade",
23 | arcade: {
24 | gravity: { y: 0 },
25 | },
26 | },
27 | };
28 |
29 | export default new Phaser.Game(config);
30 |
--------------------------------------------------------------------------------
/philoagents-ui/src/scenes/MainMenu.js:
--------------------------------------------------------------------------------
1 | import { Scene } from 'phaser';
2 |
3 | export class MainMenu extends Scene {
4 | constructor() {
5 | super('MainMenu');
6 | }
7 |
8 | create() {
9 | this.add.image(0, 0, 'background').setOrigin(0, 0);
10 | this.add.image(510, 260, 'logo').setScale(0.55);
11 |
12 | const centerX = this.cameras.main.width / 2;
13 | const startY = 524;
14 | const buttonSpacing = 70;
15 |
16 | this.createButton(centerX, startY, 'Let\'s Play!', () => {
17 | this.scene.start('Game');
18 | });
19 |
20 | this.createButton(centerX, startY + buttonSpacing, 'Instructions', () => {
21 | this.showInstructions();
22 | });
23 |
24 | this.createButton(centerX, startY + buttonSpacing * 2, 'Support Philoagents', () => {
25 | window.open('https://github.com/neural-maze/philoagents-course', '_blank');
26 | });
27 | }
28 |
29 | createButton(x, y, text, callback) {
30 | const buttonWidth = 350;
31 | const buttonHeight = 60;
32 | const cornerRadius = 20;
33 | const maxFontSize = 28;
34 | const padding = 10;
35 |
36 | const shadow = this.add.graphics();
37 | shadow.fillStyle(0x666666, 1);
38 | shadow.fillRoundedRect(x - buttonWidth / 2 + 4, y - buttonHeight / 2 + 4, buttonWidth, buttonHeight, cornerRadius);
39 |
40 | const button = this.add.graphics();
41 | button.fillStyle(0xffffff, 1);
42 | button.fillRoundedRect(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
43 | button.setInteractive(
44 | new Phaser.Geom.Rectangle(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight),
45 | Phaser.Geom.Rectangle.Contains
46 | );
47 |
48 | let fontSize = maxFontSize;
49 | let buttonText;
50 | do {
51 | if (buttonText) buttonText.destroy();
52 |
53 | buttonText = this.add.text(x, y, text, {
54 | fontSize: `${fontSize}px`,
55 | fontFamily: 'Arial',
56 | color: '#000000',
57 | fontStyle: 'bold'
58 | }).setOrigin(0.5);
59 |
60 | fontSize -= 1;
61 | } while (buttonText.width > buttonWidth - padding && fontSize > 10);
62 |
63 | button.on('pointerover', () => {
64 | this.updateButtonStyle(button, shadow, x, y, buttonWidth, buttonHeight, cornerRadius, true);
65 | buttonText.y -= 2;
66 | });
67 |
68 | button.on('pointerout', () => {
69 | this.updateButtonStyle(button, shadow, x, y, buttonWidth, buttonHeight, cornerRadius, false);
70 | buttonText.y += 2;
71 | });
72 |
73 | button.on('pointerdown', callback);
74 |
75 | return { button, shadow, text: buttonText };
76 | }
77 |
78 | updateButtonStyle(button, shadow, x, y, width, height, radius, isHover) {
79 | button.clear();
80 | shadow.clear();
81 |
82 | if (isHover) {
83 | button.fillStyle(0x87CEEB, 1);
84 | shadow.fillStyle(0x888888, 1);
85 | shadow.fillRoundedRect(x - width / 2 + 2, y - height / 2 + 2, width, height, radius);
86 | } else {
87 | button.fillStyle(0xffffff, 1);
88 | shadow.fillStyle(0x666666, 1);
89 | shadow.fillRoundedRect(x - width / 2 + 4, y - height / 2 + 4, width, height, radius);
90 | }
91 |
92 | button.fillRoundedRect(x - width / 2, y - height / 2, width, height, radius);
93 | }
94 |
95 | showInstructions() {
96 | const width = this.cameras.main.width;
97 | const height = this.cameras.main.height;
98 | const centerX = width / 2;
99 | const centerY = height / 2;
100 |
101 | const elements = this.createInstructionPanel(centerX, centerY);
102 |
103 | const instructionContent = this.addInstructionContent(centerX, centerY, elements.panel);
104 | elements.title = instructionContent.title;
105 | elements.textElements = instructionContent.textElements;
106 |
107 | const closeElements = this.addCloseButton(centerX, centerY + 79, () => {
108 | this.destroyInstructionElements(elements);
109 | });
110 | elements.closeButton = closeElements.button;
111 | elements.closeText = closeElements.text;
112 |
113 | elements.overlay.on('pointerdown', () => {
114 | this.destroyInstructionElements(elements);
115 | });
116 | }
117 |
118 | createInstructionPanel(centerX, centerY) {
119 | const overlay = this.add.graphics();
120 | overlay.fillStyle(0x000000, 0.7);
121 | overlay.fillRect(0, 0, this.cameras.main.width, this.cameras.main.height);
122 | overlay.setInteractive(
123 | new Phaser.Geom.Rectangle(0, 0, this.cameras.main.width, this.cameras.main.height),
124 | Phaser.Geom.Rectangle.Contains
125 | );
126 |
127 | const panel = this.add.graphics();
128 | panel.fillStyle(0xffffff, 1);
129 | panel.fillRoundedRect(centerX - 200, centerY - 150, 400, 300, 20);
130 | panel.lineStyle(4, 0x000000, 1);
131 | panel.strokeRoundedRect(centerX - 200, centerY - 150, 400, 300, 20);
132 |
133 | return { overlay, panel };
134 | }
135 |
136 | addInstructionContent(centerX, centerY, panel) {
137 | const title = this.add.text(centerX, centerY - 110, 'INSTRUCTIONS', {
138 | fontSize: '28px',
139 | fontFamily: 'Arial',
140 | color: '#000000',
141 | fontStyle: 'bold'
142 | }).setOrigin(0.5);
143 |
144 | const instructions = [
145 | 'Arrow keys for moving',
146 | 'SPACE for talking to philosophers',
147 | 'ESC for closing the dialogue'
148 | ];
149 |
150 | const textElements = [];
151 | let yPos = centerY - 59;
152 | instructions.forEach(instruction => {
153 | textElements.push(
154 | this.add.text(centerX, yPos, instruction, {
155 | fontSize: '22px',
156 | fontFamily: 'Arial',
157 | color: '#000000'
158 | }).setOrigin(0.5)
159 | );
160 | yPos += 40;
161 | });
162 |
163 | return { title, textElements };
164 | }
165 |
166 | addCloseButton(x, y, callback) {
167 | const adjustedY = y + 10;
168 |
169 | const buttonWidth = 120;
170 | const buttonHeight = 40;
171 | const cornerRadius = 10;
172 |
173 | const closeButton = this.add.graphics();
174 | closeButton.fillStyle(0x87CEEB, 1);
175 | closeButton.fillRoundedRect(x - buttonWidth / 2, adjustedY - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
176 | closeButton.lineStyle(2, 0x000000, 1);
177 | closeButton.strokeRoundedRect(x - buttonWidth / 2, adjustedY - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
178 |
179 | const closeText = this.add.text(x, adjustedY, 'Close', {
180 | fontSize: '20px',
181 | fontFamily: 'Arial',
182 | color: '#000000',
183 | fontStyle: 'bold'
184 | }).setOrigin(0.5);
185 |
186 | closeButton.setInteractive(
187 | new Phaser.Geom.Rectangle(x - buttonWidth / 2, adjustedY - buttonHeight / 2, buttonWidth, buttonHeight),
188 | Phaser.Geom.Rectangle.Contains
189 | );
190 |
191 | closeButton.on('pointerover', () => {
192 | closeButton.clear();
193 | closeButton.fillStyle(0x5CACEE, 1);
194 | closeButton.fillRoundedRect(x - buttonWidth / 2, adjustedY - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
195 | closeButton.lineStyle(2, 0x000000, 1);
196 | closeButton.strokeRoundedRect(x - buttonWidth / 2, adjustedY - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
197 | });
198 |
199 | closeButton.on('pointerout', () => {
200 | closeButton.clear();
201 | closeButton.fillStyle(0x87CEEB, 1);
202 | closeButton.fillRoundedRect(x - buttonWidth / 2, adjustedY - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
203 | closeButton.lineStyle(2, 0x000000, 1);
204 | closeButton.strokeRoundedRect(x - buttonWidth / 2, adjustedY - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
205 | });
206 |
207 | closeButton.on('pointerdown', callback);
208 |
209 | return { button: closeButton, text: closeText };
210 | }
211 |
212 | destroyInstructionElements(elements) {
213 | elements.overlay.destroy();
214 | elements.panel.destroy();
215 | elements.title.destroy();
216 |
217 | elements.textElements.forEach(text => text.destroy());
218 |
219 | elements.closeButton.destroy();
220 | elements.closeText.destroy();
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/philoagents-ui/src/scenes/PauseMenu.js:
--------------------------------------------------------------------------------
1 | import { Scene } from 'phaser';
2 | import ApiService from '../services/ApiService';
3 |
4 | export class PauseMenu extends Scene {
5 | constructor() {
6 | super('PauseMenu');
7 | }
8 |
9 | create() {
10 | const overlay = this.add.graphics();
11 | overlay.fillStyle(0x000000, 0.7);
12 | overlay.fillRect(0, 0, this.cameras.main.width, this.cameras.main.height);
13 |
14 | const centerX = this.cameras.main.width / 2;
15 | const centerY = this.cameras.main.height / 2;
16 |
17 | const panel = this.add.graphics();
18 | panel.fillStyle(0xffffff, 1);
19 | panel.fillRoundedRect(centerX - 200, centerY - 150, 400, 300, 20);
20 | panel.lineStyle(4, 0x000000, 1);
21 | panel.strokeRoundedRect(centerX - 200, centerY - 150, 400, 300, 20);
22 |
23 | this.add.text(centerX, centerY - 120, 'GAME PAUSED', {
24 | fontSize: '28px',
25 | fontFamily: 'Arial',
26 | color: '#000000',
27 | fontStyle: 'bold'
28 | }).setOrigin(0.5);
29 |
30 | const buttonY = centerY - 50;
31 | const buttonSpacing = 70;
32 |
33 | this.createButton(centerX, buttonY, 'Resume Game', () => {
34 | this.resumeGame();
35 | });
36 |
37 | this.createButton(centerX, buttonY + buttonSpacing, 'Main Menu', () => {
38 | this.returnToMainMenu();
39 | });
40 |
41 | this.createButton(centerX, buttonY + buttonSpacing * 2, 'Reset Game', () => {
42 | this.resetGame();
43 | });
44 |
45 | this.input.keyboard.on('keydown-ESC', () => {
46 | this.resumeGame();
47 | });
48 | }
49 |
50 | createButton(x, y, text, callback) {
51 | const buttonWidth = 250;
52 | const buttonHeight = 50;
53 | const cornerRadius = 15;
54 |
55 | const shadow = this.add.graphics();
56 | shadow.fillStyle(0x000000, 0.4);
57 | shadow.fillRoundedRect(x - buttonWidth / 2 + 5, y - buttonHeight / 2 + 5, buttonWidth, buttonHeight, cornerRadius);
58 |
59 | const button = this.add.graphics();
60 | button.fillStyle(0x4a90e2, 1);
61 | button.lineStyle(2, 0x3a70b2, 1);
62 | button.fillRoundedRect(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
63 | button.strokeRoundedRect(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
64 | button.setInteractive(
65 | new Phaser.Geom.Rectangle(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight),
66 | Phaser.Geom.Rectangle.Contains
67 | );
68 |
69 | const buttonText = this.add.text(x, y, text, {
70 | fontSize: '22px',
71 | fontFamily: 'Arial',
72 | color: '#FFFFFF',
73 | fontStyle: 'bold'
74 | }).setOrigin(0.5);
75 |
76 | button.on('pointerover', () => {
77 | button.clear();
78 | button.fillStyle(0x5da0f2, 1);
79 | button.lineStyle(2, 0x3a70b2, 1);
80 | button.fillRoundedRect(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
81 | button.strokeRoundedRect(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
82 | buttonText.y -= 2;
83 | });
84 |
85 | button.on('pointerout', () => {
86 | button.clear();
87 | button.fillStyle(0x4a90e2, 1);
88 | button.lineStyle(2, 0x3a70b2, 1);
89 | button.fillRoundedRect(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
90 | button.strokeRoundedRect(x - buttonWidth / 2, y - buttonHeight / 2, buttonWidth, buttonHeight, cornerRadius);
91 | buttonText.y += 2;
92 | });
93 |
94 | button.on('pointerdown', callback);
95 |
96 | return { button, shadow, text: buttonText };
97 | }
98 |
99 | resumeGame() {
100 | this.scene.resume('Game');
101 | this.scene.stop();
102 | }
103 |
104 | returnToMainMenu() {
105 | this.scene.stop('Game');
106 | this.scene.start('MainMenu');
107 | }
108 |
109 | async resetGame() {
110 | try {
111 | await ApiService.resetMemory();
112 |
113 | this.scene.stop('Game');
114 | this.scene.start('Game');
115 | this.scene.stop();
116 | } catch (error) {
117 | console.error('Failed to reset game:', error);
118 |
119 | const centerX = this.cameras.main.width / 2;
120 | const centerY = this.cameras.main.height / 2 + 120;
121 |
122 | const errorText = this.add.text(centerX, centerY, 'Failed to reset game. Try again.', {
123 | fontSize: '16px',
124 | fontFamily: 'Arial',
125 | color: '#FF0000'
126 | }).setOrigin(0.5);
127 |
128 | this.time.delayedCall(3000, () => {
129 | errorText.destroy();
130 | });
131 | }
132 | }
133 | }
--------------------------------------------------------------------------------
/philoagents-ui/src/scenes/Preloader.js:
--------------------------------------------------------------------------------
1 | import { Scene } from 'phaser';
2 |
3 | export class Preloader extends Scene
4 | {
5 | constructor ()
6 | {
7 | super('Preloader');
8 | }
9 |
10 | preload ()
11 | {
12 | this.load.setPath('assets');
13 |
14 | // General assets
15 | this.load.image('background', 'talking_philosophers.jpg');
16 | this.load.image('logo', 'logo.png');
17 |
18 | // Tilesets
19 | this.load.image("tuxmon-tiles", "tilesets/tuxmon-sample-32px-extruded.png");
20 | this.load.image("greece-tiles", "tilesets/ancient_greece_tileset.png");
21 | this.load.image("plant-tiles", "tilesets/plant.png");
22 |
23 | // Tilemap
24 | this.load.tilemapTiledJSON("map", "tilemaps/philoagents-town.json");
25 |
26 | // Character assets
27 | this.load.atlas("sophia", "characters/sophia/atlas.png", "characters/sophia/atlas.json");
28 | this.load.atlas("socrates", "characters/socrates/atlas.png", "characters/socrates/atlas.json");
29 | this.load.atlas("plato", "characters/plato/atlas.png", "characters/plato/atlas.json");
30 | this.load.atlas("aristotle", "characters/aristotle/atlas.png", "characters/aristotle/atlas.json");
31 | this.load.atlas("descartes", "characters/descartes/atlas.png", "characters/descartes/atlas.json");
32 | this.load.atlas("leibniz", "characters/leibniz/atlas.png", "characters/leibniz/atlas.json");
33 | this.load.atlas("ada_lovelace", "characters/ada/atlas.png", "characters/ada/atlas.json");
34 | this.load.atlas("turing", "characters/turing/atlas.png", "characters/turing/atlas.json");
35 | this.load.atlas("searle", "characters/searle/atlas.png", "characters/searle/atlas.json");
36 | this.load.atlas("chomsky", "characters/chomsky/atlas.png", "characters/chomsky/atlas.json");
37 | this.load.atlas("dennett", "characters/dennett/atlas.png", "characters/dennett/atlas.json");
38 | this.load.atlas("miguel", "characters/miguel/atlas.png", "characters/miguel/atlas.json");
39 | this.load.atlas("paul", "characters/paul/atlas.png", "characters/paul/atlas.json");
40 | }
41 |
42 | create ()
43 | {
44 | this.scene.start('MainMenu');
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/philoagents-ui/src/services/ApiService.js:
--------------------------------------------------------------------------------
1 | class ApiService {
2 | constructor() {
3 | const isHttps = window.location.protocol === 'https:';
4 |
5 | if (isHttps) {
6 | console.log('Using GitHub Codespaces');
7 | const currentHostname = window.location.hostname;
8 | this.apiUrl = `https://${currentHostname.replace('8080', '8000')}`;
9 | } else {
10 | this.apiUrl = 'http://localhost:8000';
11 | }
12 | }
13 |
14 | async request(endpoint, method, data) {
15 | const url = `${this.apiUrl}${endpoint}`;
16 | const options = {
17 | method,
18 | headers: {
19 | 'Content-Type': 'application/json',
20 | },
21 | body: data ? JSON.stringify(data) : undefined,
22 | };
23 |
24 | const response = await fetch(url, options);
25 |
26 | if (!response.ok) {
27 | throw new Error(`API error: ${response.status} ${response.statusText}`);
28 | }
29 |
30 | return response.json();
31 | }
32 |
33 | async sendMessage(philosopher, message) {
34 | try {
35 | const data = await this.request('/chat', 'POST', {
36 | message,
37 | philosopher_id: philosopher.id
38 | });
39 |
40 | return data.response;
41 | } catch (error) {
42 | console.error('Error sending message to API:', error);
43 | return this.getFallbackResponse(philosopher);
44 | }
45 | }
46 |
47 | getFallbackResponse(philosopher) {
48 | return `I'm sorry, ${philosopher.name || 'the philosopher'} is unavailable at the moment. Please try again later.`;
49 | }
50 |
51 | async resetMemory() {
52 | try {
53 | const response = await fetch(`${this.apiUrl}/reset-memory`, {
54 | method: 'POST',
55 | headers: {
56 | 'Content-Type': 'application/json'
57 | }
58 | });
59 |
60 | if (!response.ok) {
61 | throw new Error('Failed to reset memory');
62 | }
63 |
64 | return await response.json();
65 | } catch (error) {
66 | console.error('Error resetting memory:', error);
67 | throw error;
68 | }
69 | }
70 | }
71 |
72 | export default new ApiService();
--------------------------------------------------------------------------------
/philoagents-ui/src/services/WebSocketApiService.js:
--------------------------------------------------------------------------------
1 | class WebSocketApiService {
2 | constructor() {
3 | // Initialize connection-related properties
4 | this.initializeConnectionProperties();
5 |
6 | // Set up WebSocket URL based on environment
7 | this.baseUrl = this.determineWebSocketBaseUrl();
8 | }
9 |
10 | initializeConnectionProperties() {
11 | this.socket = null;
12 | this.messageCallbacks = new Map();
13 | this.connected = false;
14 | this.connectionPromise = null;
15 | this.connectionTimeout = 10000;
16 | }
17 |
18 | determineWebSocketBaseUrl() {
19 | const isHttps = window.location.protocol === 'https:';
20 |
21 | if (isHttps) {
22 | console.log('Using GitHub Codespaces');
23 | const currentHostname = window.location.hostname;
24 | return `ws://${currentHostname.replace('8080', '8000')}`;
25 | }
26 |
27 | return 'ws://localhost:8000';
28 | }
29 |
30 | connect() {
31 | if (this.connectionPromise) {
32 | return this.connectionPromise;
33 | }
34 |
35 | this.connectionPromise = new Promise((resolve, reject) => {
36 | const timeoutId = setTimeout(() => {
37 | if (this.socket) {
38 | this.socket.close();
39 | }
40 | this.connectionPromise = null;
41 | reject(new Error('WebSocket connection timeout'));
42 | }, this.connectionTimeout);
43 |
44 | this.socket = new WebSocket(`${this.baseUrl}/ws/chat`);
45 |
46 | this.socket.onopen = () => {
47 | console.log('WebSocket connection established');
48 | this.connected = true;
49 | clearTimeout(timeoutId);
50 | resolve();
51 | };
52 |
53 | this.socket.onmessage = this.handleMessage.bind(this);
54 |
55 | this.socket.onerror = (error) => {
56 | console.error('WebSocket error:', error);
57 | clearTimeout(timeoutId);
58 | this.connectionPromise = null;
59 | reject(error);
60 | };
61 |
62 | this.socket.onclose = () => {
63 | console.log('WebSocket connection closed');
64 | this.connected = false;
65 | this.connectionPromise = null;
66 | };
67 | });
68 |
69 | return this.connectionPromise;
70 | }
71 |
72 | handleMessage(event) {
73 | const data = JSON.parse(event.data);
74 |
75 | if (data.error) {
76 | console.error('WebSocket error:', data.error);
77 | return;
78 | }
79 |
80 | if (data.streaming !== undefined) {
81 | this.handleStreamingUpdate(data.streaming);
82 | return;
83 | }
84 |
85 | if (data.chunk) {
86 | this.triggerCallback('chunk', data.chunk);
87 | return;
88 | }
89 |
90 | if (data.response) {
91 | this.triggerCallback('message', data.response);
92 | }
93 | }
94 |
95 | handleStreamingUpdate(isStreaming) {
96 | const streamingCallback = this.messageCallbacks.get('streaming');
97 | if (streamingCallback) {
98 | streamingCallback(isStreaming);
99 | }
100 | }
101 |
102 | triggerCallback(type, data) {
103 | const callback = this.messageCallbacks.get(type);
104 | if (callback) {
105 | callback(data);
106 | }
107 | }
108 |
109 | async sendMessage(philosopher, message, callbacks = {}) {
110 | try {
111 | if (!this.connected) {
112 | await this.connect();
113 | }
114 |
115 | this.registerCallbacks(callbacks);
116 |
117 | this.socket.send(JSON.stringify({
118 | message: message,
119 | philosopher_id: philosopher.id
120 | }));
121 | } catch (error) {
122 | console.error('Error sending message via WebSocket:', error);
123 | return this.getFallbackResponse(philosopher);
124 | }
125 | }
126 |
127 | registerCallbacks(callbacks) {
128 | if (callbacks.onMessage) {
129 | this.messageCallbacks.set('message', callbacks.onMessage);
130 | }
131 |
132 | if (callbacks.onStreamingStart) {
133 | this.messageCallbacks.set('streaming', (isStreaming) => {
134 | if (isStreaming) {
135 | callbacks.onStreamingStart();
136 | } else if (callbacks.onStreamingEnd) {
137 | callbacks.onStreamingEnd();
138 | }
139 | });
140 | }
141 |
142 | if (callbacks.onChunk) {
143 | this.messageCallbacks.set('chunk', callbacks.onChunk);
144 | }
145 | }
146 |
147 | getFallbackResponse(philosopher) {
148 | return "I'm so tired right now, I can't talk. I'm going to sleep now.";
149 | }
150 |
151 | disconnect() {
152 | if (this.socket) {
153 | this.socket.close();
154 | this.connected = false;
155 | this.connectionPromise = null;
156 | this.messageCallbacks.clear();
157 | }
158 | }
159 | }
160 |
161 | export default new WebSocketApiService();
--------------------------------------------------------------------------------
/philoagents-ui/webpack/config.js:
--------------------------------------------------------------------------------
1 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 | const path = require("path");
4 | const webpack = require("webpack");
5 |
6 | module.exports = {
7 | mode: "development",
8 | devtool: "eval-source-map",
9 | entry: "./src/main.js",
10 | output: {
11 | path: path.resolve(process.cwd(), 'dist'),
12 | filename: "bundle.min.js"
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.js$/,
18 | exclude: /node_modules/,
19 | use: {
20 | loader: "babel-loader"
21 | }
22 | },
23 | {
24 | test: [/\.vert$/, /\.frag$/],
25 | use: "raw-loader"
26 | },
27 | {
28 | test: /\.(gif|png|jpe?g|svg|xml|glsl)$/i,
29 | use: "file-loader"
30 | }
31 | ]
32 | },
33 | plugins: [
34 | new CleanWebpackPlugin({
35 | cleanOnceBeforeBuildPatterns: [path.join(__dirname, "dist/**/*")]
36 | }),
37 | new webpack.DefinePlugin({
38 | "typeof CANVAS_RENDERER": JSON.stringify(true),
39 | "typeof WEBGL_RENDERER": JSON.stringify(true),
40 | "typeof WEBGL_DEBUG": JSON.stringify(true),
41 | "typeof EXPERIMENTAL": JSON.stringify(true),
42 | "typeof PLUGIN_3D": JSON.stringify(false),
43 | "typeof PLUGIN_CAMERA3D": JSON.stringify(false),
44 | "typeof PLUGIN_FBINSTANT": JSON.stringify(false),
45 | "typeof FEATURE_SOUND": JSON.stringify(true)
46 | }),
47 | new HtmlWebpackPlugin({
48 | template: "./index.html"
49 | })
50 | ]
51 | };
52 |
--------------------------------------------------------------------------------
/philoagents-ui/webpack/config.prod.js:
--------------------------------------------------------------------------------
1 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
2 | const CopyPlugin = require('copy-webpack-plugin');
3 | const HtmlWebpackPlugin = require("html-webpack-plugin");
4 | const path = require("path");
5 | const TerserPlugin = require("terser-webpack-plugin");
6 | const webpack = require("webpack");
7 |
8 | const line = "---------------------------------------------------------";
9 | const msg = `❤️❤️❤️ Tell us about your game! - games@phaser.io ❤️❤️❤️`;
10 | process.stdout.write(`${line}\n${msg}\n${line}\n`);
11 |
12 | module.exports = {
13 | mode: "production",
14 | entry: "./src/main.js",
15 | output: {
16 | path: path.resolve(process.cwd(), 'dist'),
17 | filename: "./bundle.min.js"
18 | },
19 | devtool: false,
20 | performance: {
21 | maxEntrypointSize: 2500000,
22 | maxAssetSize: 1200000
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.js$/,
28 | exclude: /node_modules/,
29 | use: {
30 | loader: "babel-loader"
31 | }
32 | },
33 | {
34 | test: [/\.vert$/, /\.frag$/],
35 | use: "raw-loader"
36 | },
37 | {
38 | test: /\.(gif|png|jpe?g|svg|xml|glsl)$/i,
39 | use: "file-loader"
40 | }
41 | ]
42 | },
43 | optimization: {
44 | minimizer: [
45 | new TerserPlugin({
46 | terserOptions: {
47 | output: {
48 | comments: false
49 | }
50 | }
51 | })
52 | ]
53 | },
54 | plugins: [
55 | new CleanWebpackPlugin(),
56 | new webpack.DefinePlugin({
57 | "typeof CANVAS_RENDERER": JSON.stringify(true),
58 | "typeof WEBGL_RENDERER": JSON.stringify(true),
59 | "typeof WEBGL_DEBUG": JSON.stringify(false),
60 | "typeof EXPERIMENTAL": JSON.stringify(false),
61 | "typeof PLUGIN_3D": JSON.stringify(false),
62 | "typeof PLUGIN_CAMERA3D": JSON.stringify(false),
63 | "typeof PLUGIN_FBINSTANT": JSON.stringify(false),
64 | "typeof FEATURE_SOUND": JSON.stringify(true)
65 | }),
66 | new HtmlWebpackPlugin({
67 | template: "./index.html"
68 | }),
69 | new CopyPlugin({
70 | patterns: [
71 | { from: 'public/assets', to: 'assets' },
72 | { from: 'public/favicon.png', to: 'favicon.png' },
73 | { from: 'public/style.css', to: 'style.css' }
74 | ],
75 | }),
76 | ]
77 | };
78 |
--------------------------------------------------------------------------------
/static/diagrams/agent_memory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/agent_memory.png
--------------------------------------------------------------------------------
/static/diagrams/episode_1_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/episode_1_play.png
--------------------------------------------------------------------------------
/static/diagrams/episode_2_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/episode_2_play.png
--------------------------------------------------------------------------------
/static/diagrams/episode_3_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/episode_3_play.png
--------------------------------------------------------------------------------
/static/diagrams/episode_4_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/episode_4_play.png
--------------------------------------------------------------------------------
/static/diagrams/episode_5_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/episode_5_play.png
--------------------------------------------------------------------------------
/static/diagrams/episode_6_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/episode_6_play.png
--------------------------------------------------------------------------------
/static/diagrams/langgraph_agent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/langgraph_agent.png
--------------------------------------------------------------------------------
/static/diagrams/system_architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/diagrams/system_architecture.png
--------------------------------------------------------------------------------
/static/game_socrates_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/game_socrates_example.png
--------------------------------------------------------------------------------
/static/game_starting_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/game_starting_page.png
--------------------------------------------------------------------------------
/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/logo.png
--------------------------------------------------------------------------------
/static/opik_evaluation_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/opik_evaluation_example.png
--------------------------------------------------------------------------------
/static/opik_monitoring_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/opik_monitoring_example.png
--------------------------------------------------------------------------------
/static/sponsors/groq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/sponsors/groq.png
--------------------------------------------------------------------------------
/static/sponsors/mongo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/sponsors/mongo.png
--------------------------------------------------------------------------------
/static/sponsors/opik.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/sponsors/opik.png
--------------------------------------------------------------------------------
/static/thumbnails/episode_1_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/thumbnails/episode_1_play.png
--------------------------------------------------------------------------------
/static/thumbnails/episode_2_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/thumbnails/episode_2_play.png
--------------------------------------------------------------------------------
/static/thumbnails/episode_3_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/thumbnails/episode_3_play.png
--------------------------------------------------------------------------------
/static/thumbnails/episode_4_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/thumbnails/episode_4_play.png
--------------------------------------------------------------------------------
/static/thumbnails/episode_5_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/thumbnails/episode_5_play.png
--------------------------------------------------------------------------------
/static/thumbnails/full_course_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neural-maze/philoagents-course/7792f5546048e7c109881fa17df4a9039a9d61b5/static/thumbnails/full_course_play.png
--------------------------------------------------------------------------------