├── .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 | 8 | 15 | 16 |
4 | 5 | The Neural Maze Logo 6 | 7 | 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 |
17 | 18 |

19 | 20 | Subscribe Now 21 | 22 |

23 | 24 | 25 | 26 | 31 | 37 | 38 |
27 | 28 | Decoding ML Logo 29 | 30 | 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 |
39 | 40 |

41 | 42 | Subscribe Now 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 | ![Philosopher Town](static/game_starting_page.png) 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 | ![Philosopher Town](static/game_socrates_example.png) 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 | ![Opik](static/opik_monitoring_example.png) 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 | ![Opik](static/opik_evaluation_example.png) 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 | ![Philosopher Town](public/assets/game_screenshot.png) 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 | ![Philosopher Town](public/assets/philoagents_town.png) 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 | ![Ada Image](public/assets/sprite_image.png) 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 |
13 |
14 |
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 --------------------------------------------------------------------------------