├── .docker └── Dockerfile ├── .editorconfig ├── .env.sample ├── .github ├── FUNDING.yml └── workflows │ └── docker-image.yml ├── .gitignore ├── .rr.yaml ├── .styleci.yml ├── Makefile ├── README.md ├── app.php ├── app ├── config │ ├── cache.php │ ├── cycle.php │ ├── database.php │ ├── migration.php │ └── scaffolder.php ├── database │ ├── Factory │ │ └── .gitkeep │ └── Migration │ │ ├── .gitkeep │ │ └── 20240825.195024_0_0_default_create_chat_sessions.php └── src │ ├── Agents │ ├── AgentsCaller │ │ ├── AskAgentInput.php │ │ └── AskAgentTool.php │ ├── CodeReviewer │ │ ├── CodeReviewAgent.php │ │ ├── CodeReviewAgentFactory.php │ │ ├── ListProjectInput.php │ │ ├── ListProjectTool.php │ │ ├── ReadFileInput.php │ │ ├── ReadFileTool.php │ │ ├── ReviewInput.php │ │ └── ReviewTool.php │ ├── Delivery │ │ ├── DeliveryAgent.php │ │ ├── DeliveryAgentFactory.php │ │ ├── DeliveryDateInput.php │ │ ├── GetDeliveryDateTool.php │ │ ├── GetOrderNumberTool.php │ │ ├── GetProfileTool.php │ │ ├── OrderNumberInput.php │ │ ├── ProfileInput.php │ │ └── StatusCheckOutput.php │ ├── DynamicMemoryTool │ │ ├── DynamicMemoryInput.php │ │ ├── DynamicMemoryService.php │ │ ├── DynamicMemoryTool.php │ │ └── Memories.php │ └── TaskSplitter │ │ ├── GetProjectDescription.php │ │ ├── ProjectDescriptionInput.php │ │ ├── TaskCreateInput.php │ │ ├── TaskCreateTool.php │ │ ├── TaskSplitterAgent.php │ │ └── TaskSplitterAgentFactory.php │ ├── Application │ ├── AgentsLocator.php │ ├── Assert.php │ ├── Bootloader │ │ ├── AgentsBootloader.php │ │ ├── AgentsChatBootloader.php │ │ ├── AppBootloader.php │ │ ├── EventsBootloader.php │ │ ├── Infrastructure │ │ │ ├── CloudStorageBootloader.php │ │ │ ├── ConsoleBootloader.php │ │ │ ├── CycleOrmBootloader.php │ │ │ ├── LogsBootloader.php │ │ │ ├── RoadRunnerBootloader.php │ │ │ └── SecurityBootloader.php │ │ ├── PersistenceBootloader.php │ │ └── SmartHomeBootloader.php │ ├── Entity │ │ ├── Json.php │ │ └── Uuid.php │ ├── Exception │ │ └── InvalidArgumentException.php │ ├── Kernel.php │ └── ToolsLocator.php │ ├── Domain │ └── Chat │ │ ├── EntityManagerInterface.php │ │ ├── History.php │ │ ├── PromptGenerator │ │ ├── Dump.php │ │ └── SessionContextInjector.php │ │ ├── Session.php │ │ ├── SessionRepositoryInterface.php │ │ └── SimpleChatService.php │ ├── Endpoint │ └── Console │ │ ├── AgentsListCommand.php │ │ ├── ChatCommand.php │ │ ├── ChatWindowCommand.php │ │ ├── DisplaySmartHomeStatusCommand.php │ │ ├── KnowledgeBaseGenerator.php │ │ └── ToolListCommand.php │ └── Infrastructure │ ├── CycleOrm │ ├── Entity │ │ └── SessionEntityManager.php │ ├── Repository │ │ └── SessionRepository.php │ └── Table │ │ └── SessionTable.php │ └── RoadRunner │ ├── Chat │ ├── ChatEventsListener.php │ └── ChatHistoryRepository.php │ └── SmartHome │ └── DeviceStateManager.php ├── composer.json ├── docker-compose.yaml ├── knowledge-base ├── agents-example.txt ├── app-example.txt ├── cli-chat.txt ├── domain-layer.txt ├── mermaid-schema.md ├── packages-layer.txt └── what-is-llm.md ├── phpunit.xml ├── psalm.xml └── tests ├── App └── TestKernel.php ├── DatabaseTestCase.php ├── Feature └── .gitignore ├── TestCase.php └── Unit └── Application └── Entity ├── JsonTest.php └── UuidTest.php /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ROAD_RUNNER_IMAGE=2024.2.0 2 | ARG DOLT_IMAGE=1.42.8 3 | 4 | # Build dolt binary 5 | FROM dolthub/dolt:$DOLT_IMAGE as dolt 6 | # Build rr binary 7 | FROM ghcr.io/roadrunner-server/roadrunner:$ROAD_RUNNER_IMAGE as rr 8 | # Clone the project 9 | FROM alpine/git as git 10 | 11 | ARG REPOSITORY=https://github.com/llm-agents-php/sample-app.git 12 | ARG BRANCH=main 13 | RUN git clone -b $BRANCH $REPOSITORY /app 14 | 15 | FROM php:8.3-cli-alpine3.18 16 | 17 | RUN apk add --no-cache $PHPIZE_DEPS \ 18 | curl \ 19 | libcurl \ 20 | wget \ 21 | libzip-dev \ 22 | libmcrypt-dev \ 23 | libxslt-dev \ 24 | libxml2-dev \ 25 | openssl-dev \ 26 | icu-dev \ 27 | zip \ 28 | unzip \ 29 | linux-headers 30 | 31 | RUN docker-php-ext-install \ 32 | opcache \ 33 | zip \ 34 | dom \ 35 | sockets 36 | 37 | # PDO database drivers support 38 | RUN docker-php-ext-install pdo_mysql 39 | 40 | COPY --from=git /app /app 41 | COPY --from=rr /usr/bin/rr /app 42 | COPY --from=dolt /usr/local/bin/dolt /app 43 | COPY --from=composer /usr/bin/composer /usr/bin/composer 44 | 45 | ARG APP_VERSION=v1.0 46 | ENV COMPOSER_ALLOW_SUPERUSER=1 47 | 48 | WORKDIR /app 49 | 50 | RUN composer config --no-plugins allow-plugins.spiral/composer-publish-plugin false 51 | RUN composer install --no-dev 52 | 53 | WORKDIR /app 54 | 55 | RUN mkdir .db 56 | RUN ./dolt --data-dir=.db sql -q "create database llm;" 57 | 58 | ENV APP_ENV=prod 59 | ENV DEBUG=false 60 | ENV VERBOSITY_LEVEL=verbose 61 | ENV ENCRYPTER_KEY=def00000232ae92c8e8ec0699093fa06ce014cd48d39c3c62c279dd947db084e56ee48b5c91cebc1c5abe53f7755021d09043757561c244c1c0c765cfeb5db33eb45a903 62 | ENV MONOLOG_DEFAULT_CHANNEL=roadrunner 63 | ENV MONOLOG_DEFAULT_LEVEL=INFO 64 | ENV APP_VERSION=$APP_VERSION 65 | ENV RR_LOG_LEVEL=error 66 | 67 | LABEL org.opencontainers.image.source=$REPOSITORY 68 | LABEL org.opencontainers.image.description="LL Agents PHP" 69 | LABEL org.opencontainers.image.licenses=MIT 70 | 71 | CMD ./rr serve -c .rr.yaml 72 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.yml] 12 | indent_size = 2 13 | 14 | [*.yaml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Environment (prod or local) 2 | APP_ENV=local 3 | 4 | # Debug mode set to TRUE disables view caching and enables higher verbosity 5 | DEBUG=true 6 | 7 | # Verbosity level 8 | VERBOSITY_LEVEL=verbose # basic, verbose, debug 9 | 10 | # Set to an application specific value, used to encrypt/decrypt cookies etc 11 | ENCRYPTER_KEY={encrypt-key} 12 | 13 | # Monolog 14 | MONOLOG_DEFAULT_CHANNEL=default # Use "roadrunner" channel if you want to use RoadRunner logger 15 | MONOLOG_DEFAULT_LEVEL=DEBUG # DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY 16 | 17 | # Telemetry 18 | TELEMETRY_DRIVER=null 19 | 20 | # OpenAI Key 21 | OPENAI_KEY= 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: butschster 4 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | build-release: 10 | if: "!github.event.release.prerelease" 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: 'Get Previous tag' 17 | id: previoustag 18 | uses: "WyriHaximus/github-action-get-previous-tag@v1" 19 | with: 20 | fallback: v0.1 21 | 22 | - name: Login to Docker Hub 23 | uses: docker/login-action@v2 24 | with: 25 | registry: ghcr.io 26 | username: ${{ secrets.GHCR_LOGIN }} 27 | password: ${{ secrets.GHCR_PASSWORD }} 28 | 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v2 31 | 32 | - name: Set up Docker Buildx 33 | id: buildx 34 | uses: docker/setup-buildx-action@v2 35 | 36 | - name: Build and push 37 | id: docker_build 38 | uses: docker/build-push-action@v3 39 | with: 40 | context: ./ 41 | platforms: linux/amd64,linux/arm64 42 | file: ./.docker/Dockerfile 43 | push: true 44 | build-args: | 45 | APP_VERSION=${{ steps.previoustag.outputs.tag }} 46 | tags: 47 | ghcr.io/${{ github.repository }}:latest, ghcr.io/${{ github.repository }}:${{ steps.previoustag.outputs.tag }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | runtime 4 | rr* 5 | protoc-gen-php-grpc* 6 | .env 7 | .phpunit.result.cache 8 | .phpunit.cache 9 | .deptrac.cache 10 | .phpunit.cache/ 11 | dolt 12 | .db 13 | composer.lock 14 | -------------------------------------------------------------------------------- /.rr.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | rpc: 4 | listen: tcp://127.0.0.1:6001 5 | 6 | kv: 7 | local: 8 | driver: memory 9 | config: { } 10 | 11 | server: 12 | on_init: 13 | command: 'php app.php migrate --force' 14 | command: 'php app.php' 15 | relay: pipes 16 | 17 | logs: 18 | level: ${RR_LOG_LEVEL:-error} 19 | 20 | service: 21 | dolt: 22 | service_name_in_log: true 23 | remain_after_exit: true 24 | restart_sec: 1 25 | command: "./dolt sql-server --data-dir=.db" 26 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | risky: false 2 | preset: psr12 3 | enabled: 4 | # Risky Fixers 5 | # - declare_strict_types 6 | # - void_return 7 | - ordered_class_elements 8 | - linebreak_after_opening_tag 9 | - single_quote 10 | - no_blank_lines_after_phpdoc 11 | - unary_operator_spaces 12 | - no_useless_else 13 | - no_useless_return 14 | - trailing_comma_in_multiline_array 15 | finder: 16 | exclude: 17 | - "Tests" 18 | - "installer/Application/Common/resources/tests" 19 | - "installer/Module/RoadRunnerBridge/resources/config" 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ########################### 2 | # Docker # 3 | ########################### 4 | start: 5 | docker compose up --remove-orphans -d; 6 | 7 | up: start 8 | 9 | stop: 10 | docker compose stop; 11 | 12 | down: 13 | docker compose down; 14 | 15 | restart: 16 | docker compose restart; 17 | 18 | bash: 19 | docker compose exec app /bin/sh; 20 | 21 | ########################### 22 | # Local development # 23 | ########################### 24 | init: init-db init-rr 25 | 26 | # Install dolt database 27 | init-db: 28 | if [ ! -f "dolt" ]; then \ 29 | vendor/bin/dload get dolt;\ 30 | chmod +x dolt;\ 31 | fi 32 | 33 | if [ ! -d ".db" ]; then \ 34 | mkdir -p .db; \ 35 | chmod 0777 -R .db; \ 36 | ./dolt --data-dir=.db sql -q "create database llm;"; \ 37 | fi 38 | 39 | # Install RoadRunner 40 | init-rr: 41 | if [ ! -f "rr" ]; then \ 42 | vendor/bin/rr get;\ 43 | fi 44 | 45 | clear-cache: 46 | rm -rf runtime/cache; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LLM Agents Sample App 2 | 3 | This sample application demonstrates the practical implementation and usage patterns of the LLM Agents library. 4 | 5 | > For more information about the LLM Agents package and its capabilities, please refer to 6 | > the [LLM Agents documentation](https://github.com/llm-agents-php/agents). 7 | 8 | It provides a CLI interface to interact with various AI agents, showcasing the power and flexibility of the LLM Agents 9 | package. 10 | 11 | ![image](https://github.com/user-attachments/assets/53104067-d3df-4983-8a59-435708f2b70c) 12 | 13 | ## Features 14 | 15 | - Multiple pre-configured AI agents with different capabilities 16 | - CLI interface for easy interaction with agents 17 | - Integration with OpenAI's GPT models 18 | - Database support for session persistence 19 | 20 | ## Prerequisites 21 | 22 | - PHP 8.3 or higher 23 | - Composer 24 | - Git 25 | - OpenAI API key 26 | 27 | ## Quick Start with Docker 28 | 29 | The easiest way to run the app is using our pre-built Docker image. 30 | 31 | **Follow these steps to get started:** 32 | 33 | 1. Make sure you have Docker installed on your system. 34 | 35 | 2. Run the Docker container with the following command: 36 | 37 | ```bash 38 | docker run --name chat-app -e OPENAI_KEY= ghcr.io/llm-agents-php/sample-app:1.0.0 39 | ``` 40 | 41 | or if you want to get environment variables from a file: 42 | 43 | ```bash 44 | docker run --name chat-app --env-file .env ghcr.io/llm-agents-php/sample-app:1.0.0 45 | ``` 46 | 47 | and `.env` file should look like this: 48 | 49 | ```bash 50 | OPENAI_KEY=your_api_key_here 51 | ``` 52 | 53 | > Replace `` with your OpenAI API key. 54 | 55 | 3. Once the container is running, you can interact with the app using the following command: 56 | 57 | ## Usage 58 | 59 | ### Chatting with Agents 60 | 61 | To start a chat session with an AI agent: 62 | 63 | 1. Run the following command: 64 | 65 | **Using docker container** 66 | ```bash 67 | docker exec -it chat-app php app.php chat 68 | ``` 69 | 70 | **Using local installation** 71 | ```bash 72 | php app.php chat 73 | ``` 74 | 75 | 2. You will see a list of available agents and their descriptions. Choose the desired agent by entering its number. 76 | 77 | ![image](https://github.com/user-attachments/assets/3cd223a8-3ab0-4879-9e85-83539c93003f) 78 | 79 | 3. After selecting an agent, you will see a message like this: 80 | 81 | ![image](https://github.com/user-attachments/assets/0d18ca6c-9ee9-4942-b383-fc42abf18bc7) 82 | 83 | ```bash 84 | ************************************************************ 85 | * Run the following command to see the AI response * 86 | ************************************************************ 87 | 88 | php app.php chat:session -v 89 | ``` 90 | 91 | **Using docker container** 92 | ```bash 93 | docker exec -it chat-app php app.php chat:session -v 94 | ```` 95 | > Replace `` with the actual session UUID. 96 | 97 | 98 | 4. Copy the provided command and run it in a new terminal tab. This command will show the AI response to your message. 99 | 100 | ![image](https://github.com/user-attachments/assets/1dfdfdd1-f69d-44af-afb2-807f9fa2da84) 101 | 102 | ## Available CLI Commands 103 | 104 | The sample app provides several CLI commands for interacting with agents and managing the application: 105 | 106 | - `php app.php agent:list`: List all available agents 107 | - `php app.php tool:list`: List all available tools 108 | - `php app.php chat`: Start a new chat session 109 | - `php app.php chat:session `: Continue an existing chat session 110 | - `php app.php migrate`: Execute database migrations 111 | 112 | Use the `-h` or `--help` option with any command to see more details about its usage. 113 | 114 | ## Available Agents 115 | 116 | The sample app comes with several pre-configured agents, each designed for specific tasks: 117 | 118 | ### Site Status Checker 119 | 120 | - **Key**: `site_status_checker` 121 | - **Description**: This agent specializes in checking the online status of websites. It can verify if a given URL is 122 | accessible, retrieve basic information about the site, and provide insights on potential issues if a site is 123 | offline. 124 | - **Capabilities**: 125 | - Check site availability 126 | - Retrieve DNS information 127 | - Perform ping tests 128 | - Provide troubleshooting steps for offline sites 129 | 130 | ### Order Assistant 131 | 132 | - **Key**: `order_assistant` 133 | - **Description**: This agent helps customers with order-related questions. It can retrieve order information, check 134 | delivery status, and provide customer support for e-commerce related queries. 135 | - **Capabilities**: 136 | - Retrieve order numbers 137 | - Check delivery dates 138 | - Access customer profiles 139 | - Provide personalized assistance based on customer age and preferences 140 | 141 | ### Smart Home Control Assistant 142 | 143 | - **Key**: `smart_home_control` 144 | - **Description**: This agent manages and controls various smart home devices across multiple rooms, including 145 | lights, thermostats, and TVs. 146 | - **Capabilities**: 147 | - List devices in specific rooms 148 | - Control individual devices (turn on/off, adjust settings) 149 | - Retrieve device status and details 150 | - Suggest energy-efficient settings 151 | 152 | ### Code Review Agent 153 | 154 | - **Key**: `code_review` 155 | - **Description**: This agent specializes in reviewing code. It can analyze code files, provide feedback, and 156 | suggest improvements. 157 | - **Capabilities**: 158 | - List files in a project 159 | - Read file contents 160 | - Perform code reviews 161 | - Submit review comments 162 | 163 | ### Task Splitter 164 | 165 | - **Key**: `task_splitter` 166 | - **Description**: This agent analyzes project descriptions and breaks them down into structured task lists with 167 | subtasks. 168 | - **Capabilities**: 169 | - Retrieve project descriptions 170 | - Create hierarchical task structures 171 | - Assign task priorities 172 | - Generate detailed subtasks 173 | 174 | 175 | ## Dev installation 176 | 177 | 1. Clone the repository: 178 | 179 | ```bash 180 | git clone https://github.com/llm-agents-php/sample-app.git 181 | cd sample-app 182 | ``` 183 | 184 | 2. Install dependencies: 185 | 186 | ```bash 187 | composer install 188 | ``` 189 | 190 | 3. Set up the environment: 191 | 192 | ```bash 193 | cp .env.sample .env 194 | ``` 195 | 196 | Open the `.env` file and add your OpenAI API key: 197 | 198 | ```bash 199 | OPENAI_KEY=your_api_key_here 200 | ``` 201 | 202 | 4. Initialize the project: 203 | 204 | ```bash 205 | make init 206 | ``` 207 | 208 | This command will download and set up all required binaries, including: 209 | 210 | - Dolt: A SQL database server 211 | - RoadRunner: A high-performance PHP application server 212 | 213 | ### Starting the Server 214 | 215 | To start the RoadRunner server and the Dolt database, run: 216 | 217 | ```bash 218 | ./rr serve 219 | ``` 220 | 221 | ## Knowledge Base 222 | 223 | This sample project includes a console command to generate a knowledge base, which can be useful for creating project 224 | documentation or training data for AI models like Claude. 225 | 226 | ### Creating a Project in Claude 227 | 228 | Follow these steps to create a new project in Claude using the generated knowledge base: 229 | 230 | 1. Create a New Project 231 | - Go to the Claude interface (e.g., chat.openai.com for ChatGPT). 232 | - Create a new project. 233 | 234 | 2. Add Instructions from README below 235 | 236 | 3. Upload Knowledge Base Files 237 | - Locate the `./knowledge-base` directory on your local machine. 238 | - Upload all files from this directory to Claude. 239 | - Ensure all relevant PHP files, documentation, and any other project-related files are included. 240 | 241 | 4. Test Your Project 242 | - To test if everything is set up correctly, ask Claude to create a "Weather Checker" agent. 243 | - Review the generated code and explanations provided by Claude. 244 | 245 | ### Instructing an AI with 246 | 247 | Once you've generated the knowledge base, you can use it to create new agent codebases or to provide context for 248 | AI-assisted development. Here's an example of how you might use the generated knowledge base to instruct an AI: 249 | 250 | ```prompt 251 | Create a new AI agent with the following specifications: 252 | 253 | 1. Agent Name: [Provide a descriptive name for the agent] 254 | 2. Agent Unique Key: [Provide a unique identifier for the agent, using lowercase letters, numbers, and underscores] 255 | 3. Agent Description: [Provide a detailed description of the agent's purpose, capabilities, and use cases] 256 | 4. Agent Instruction: [Provide a detailed instruction for the agent, explaining how it should behave, what its primary goals are, and any specific guidelines it should follow] 257 | 5. Tools: List the tools that would be useful for this agent. For each tool, provide: 258 | a. Tool Key: [A unique identifier for the tool] 259 | b. Tool Description: [A concise yet comprehensive explanation of the tool's functionality] 260 | c. Tool Input Schema: [Describe the input parameters for the tool in JSON format] 261 | 262 | Example Tool Format: 263 | { 264 | "key": "example_tool", 265 | "description": "This tool performs X function, useful for Y scenarios. It takes A and B as inputs and returns Z.", 266 | "input_schema": { 267 | "type": "object", 268 | "properties": { 269 | "param1": { 270 | "type": "string", 271 | "description": "Description of param1" 272 | }, 273 | "param2": { 274 | "type": "integer", 275 | "description": "Description of param2" 276 | } 277 | }, 278 | "required": ["param1", "param2"] 279 | } 280 | } 281 | 282 | 6. Agent Memory: [List any specific information or guidelines that the agent should always keep in mind] 283 | 7. Agent example prompts 284 | 8. Always use gpt-4o-mini model as a bae model for the agent 285 | 286 | Your tasks: 287 | * Generate all necessary PHP classes for Agent 288 | * Agent 289 | * AgentFactory 290 | * All necessary tools 291 | * All necessary Tool input shemas 292 | - You use PHP 8.3 with Constructor property promotion, named arguments, and do not use annotations. 293 | ``` 294 | 295 | By providing such instructions along with the generated knowledge base, you can guide AI models like Claude to create 296 | new components that align with your project's structure and coding standards. 297 | 298 | ### Generating the Knowledge Base 299 | 300 | To generate the knowledge base, run the following command: 301 | 302 | ```bash 303 | php app.php kb:generate 304 | ``` 305 | 306 | This command will create a knowledge base in the `./knowledge-base` directory. The generated knowledge base contains 307 | documentation and codebase examples that can be used, for instance, to create a project for Claude AI. 308 | 309 | ### Extending the Knowledge Base 310 | 311 | As your project grows, you may want to update the knowledge base to include new features, agents, or tools. Simply run 312 | the `kb:generate` command again to refresh the knowledge base with the latest changes in your project. 313 | 314 | This approach allows for an iterative development process where you can continuously improve and expand your agent 315 | ecosystem, leveraging both human expertise and AI assistance. 316 | 317 | ## Contributing 318 | 319 | Contributions are welcome! Please feel free to submit a Pull Request. 320 | 321 | ## License 322 | 323 | This sample app is open-source software licensed under the MIT license. 324 | -------------------------------------------------------------------------------- /app.php: -------------------------------------------------------------------------------- 1 | __DIR__], 22 | )->run(); 23 | 24 | if ($app === null) { 25 | exit(255); 26 | } 27 | 28 | $code = (int)$app->serve(); 29 | exit($code); 30 | -------------------------------------------------------------------------------- /app/config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORAGE', 'rr-local'), 7 | 8 | 'aliases' => [ 9 | 'chat-messages' => [ 10 | 'storage' => 'rr-local', 11 | 'prefix' => 'chat:', 12 | ], 13 | 'smart-home' => [ 14 | 'storage' => 'rr-local', 15 | 'prefix' => 'smart-home:', 16 | ], 17 | ], 18 | 19 | 'storages' => [ 20 | 'rr-local' => [ 21 | 'type' => 'roadrunner', 22 | 'driver' => 'local', 23 | ], 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /app/config/cycle.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'cache' => env('CYCLE_SCHEMA_CACHE', true), 9 | 10 | 'defaults' => [], 11 | 12 | 'collections' => [ 13 | 'default' => 'array', 14 | 'factories' => ['array' => new ArrayCollectionFactory()], 15 | ], 16 | 17 | 'generators' => null, 18 | ], 19 | 20 | 'warmup' => env('CYCLE_SCHEMA_WARMUP', false), 21 | ]; 22 | -------------------------------------------------------------------------------- /app/config/database.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'default' => null, 10 | 'drivers' => [ 11 | // 'runtime' => 'stdout' 12 | ], 13 | ], 14 | 15 | 'default' => 'default', 16 | 17 | 'databases' => [ 18 | 'default' => [ 19 | 'driver' => env('DB_CONNECTION', 'mysql'), 20 | ], 21 | ], 22 | 23 | 'drivers' => [ 24 | 'mysql' => new Config\MySQLDriverConfig( 25 | connection: new Config\MySQL\TcpConnectionConfig( 26 | database: env('DB_DATABASE', 'llm'), 27 | host: env('DB_HOST', '127.0.0.1'), 28 | port: (int) env('DB_PORT', 3306), 29 | user: env('DB_USERNAME', 'root'), 30 | password: env('DB_PASSWORD'), 31 | ), 32 | queryCache: true, 33 | ), 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /app/config/migration.php: -------------------------------------------------------------------------------- 1 | directory('app') . 'database/Migration/', 9 | 10 | 'table' => 'migrations', 11 | 12 | 'safe' => env('APP_ENV') !== 'prod', 13 | 14 | 'strategy' => MultipleFilesStrategy::class, 15 | 16 | 'namespace' => 'Database\\Migration', 17 | ]; 18 | -------------------------------------------------------------------------------- /app/config/scaffolder.php: -------------------------------------------------------------------------------- 1 | 'App', 10 | 11 | 'declarations' => [ 12 | Declaration\BootloaderDeclaration::TYPE => [ 13 | 'namespace' => 'Application\\Bootloader', 14 | ], 15 | Declaration\ConfigDeclaration::TYPE => [ 16 | 'namespace' => 'Application\\Config', 17 | ], 18 | Declaration\ControllerDeclaration::TYPE => [ 19 | 'namespace' => 'Endpoint\\Web', 20 | ], 21 | Declaration\FilterDeclaration::TYPE => [ 22 | 'namespace' => 'Endpoint\\Web\\Filter', 23 | ], 24 | Declaration\MiddlewareDeclaration::TYPE => [ 25 | 'namespace' => 'Endpoint\\Web\\Middleware', 26 | ], 27 | Declaration\CommandDeclaration::TYPE => [ 28 | 'namespace' => 'Endpoint\\Console', 29 | ], 30 | Declaration\JobHandlerDeclaration::TYPE => [ 31 | 'namespace' => 'Endpoint\\Job', 32 | ], 33 | ], 34 | ]; 35 | -------------------------------------------------------------------------------- /app/database/Factory/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-agents-php/sample-app/3fe05382a91bad58c3d1ffe525e8bf5546103b56/app/database/Factory/.gitkeep -------------------------------------------------------------------------------- /app/database/Migration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-agents-php/sample-app/3fe05382a91bad58c3d1ffe525e8bf5546103b56/app/database/Migration/.gitkeep -------------------------------------------------------------------------------- /app/database/Migration/20240825.195024_0_0_default_create_chat_sessions.php: -------------------------------------------------------------------------------- 1 | table('chat_sessions') 16 | ->addColumn('created_at', 'datetime', ['nullable' => false, 'defaultValue' => 'CURRENT_TIMESTAMP']) 17 | ->addColumn('updated_at', 'datetime', ['nullable' => false, 'defaultValue' => null]) 18 | ->addColumn('title', 'string', ['nullable' => true, 'defaultValue' => null, 'size' => 255]) 19 | ->addColumn('history', 'json', ['nullable' => false, 'defaultValue' => null]) 20 | ->addColumn('finished_at', 'datetime', ['nullable' => true, 'defaultValue' => null]) 21 | ->addColumn('uuid', 'uuid', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) 22 | ->addColumn('account_uuid', 'uuid', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) 23 | ->addColumn('agent_name', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 255]) 24 | ->setPrimaryKeys(['uuid']) 25 | ->create(); 26 | } 27 | 28 | public function down(): void 29 | { 30 | $this->table('chat_sessions')->drop(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/Agents/AgentsCaller/AskAgentInput.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class AskAgentTool extends PhpTool 18 | { 19 | public const NAME = 'ask_agent'; 20 | 21 | public function __construct( 22 | private readonly ExecutorInterface $executor, 23 | private readonly ToolExecutor $toolExecutor, 24 | ) { 25 | parent::__construct( 26 | name: self::NAME, 27 | inputSchema: AskAgentInput::class, 28 | description: 'Ask an agent with given name to execute a task.', 29 | ); 30 | } 31 | 32 | public function execute(object $input): string|\Stringable 33 | { 34 | $prompt = \sprintf( 35 | <<<'PROMPT' 36 | %s 37 | Important rules: 38 | - Think before responding to the user. 39 | - Don not markup the content. Only JSON is allowed. 40 | - Don't write anything except the answer using JSON schema. 41 | - Answer in JSON using this schema: 42 | %s 43 | PROMPT 44 | , 45 | $input->question, 46 | $input->outputSchema, 47 | ); 48 | 49 | // TODO: make async 50 | while (true) { 51 | $execution = $this->executor->execute(agent: $input->name, prompt: $prompt, promptContext: new Context()); 52 | $result = $execution->result; 53 | $prompt = $execution->prompt; 54 | 55 | if ($result instanceof ToolCalledResponse) { 56 | foreach ($result->tools as $tool) { 57 | $functionResult = $this->toolExecutor->execute($tool->name, $tool->arguments); 58 | 59 | $prompt = $prompt->withAddedMessage( 60 | new ToolCallResultMessage( 61 | id: $tool->id, 62 | content: [$functionResult], 63 | ), 64 | ); 65 | } 66 | 67 | continue; 68 | } 69 | 70 | break; 71 | } 72 | 73 | return \json_encode($result->content); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/Agents/CodeReviewer/CodeReviewAgent.php: -------------------------------------------------------------------------------- 1 | addMetadata( 35 | new SolutionMetadata( 36 | type: MetadataType::Memory, 37 | key: 'user_submitted_code_review', 38 | content: 'Always submit code reviews using proper tool.', 39 | ), 40 | new SolutionMetadata( 41 | type: MetadataType::Memory, 42 | key: 'code_review_tip', 43 | content: 'Always submit constructive feedback and suggestions for improvement in your code reviews.', 44 | ), 45 | new SolutionMetadata( 46 | type: MetadataType::Memory, 47 | key: 'customer_name', 48 | content: 'If you know the customer name say hello to them.', 49 | ), 50 | new SolutionMetadata( 51 | type: MetadataType::Configuration, 52 | key: Option::MaxTokens->value, 53 | content: 3000, 54 | ), 55 | ); 56 | 57 | $model = new Model(model: OpenAIModel::Gpt4oMini->value); 58 | $aggregate->addAssociation($model); 59 | 60 | $aggregate->addAssociation(new ToolLink(name: ListProjectTool::NAME)); 61 | $aggregate->addAssociation(new ToolLink(name: ReadFileTool::NAME)); 62 | $aggregate->addAssociation(new ToolLink(name: ReviewTool::NAME)); 63 | 64 | $aggregate->addAssociation(new ContextSourceLink(name: 'spiral-docs')); 65 | 66 | return $aggregate; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/Agents/CodeReviewer/CodeReviewAgentFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ListProjectTool extends PhpTool 13 | { 14 | public const NAME = 'list_project'; 15 | 16 | public function __construct() 17 | { 18 | parent::__construct( 19 | name: self::NAME, 20 | inputSchema: ListProjectInput::class, 21 | description: 'List all files in a project with the given project name.', 22 | ); 23 | } 24 | 25 | public function execute(object $input): string 26 | { 27 | // Implementation to list project files 28 | return json_encode(['files' => ['file1.php', 'file2.php']]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/Agents/CodeReviewer/ReadFileInput.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ReadFileTool extends PhpTool 13 | { 14 | public const NAME = 'read_file'; 15 | 16 | public function __construct() 17 | { 18 | parent::__construct( 19 | name: self::NAME, 20 | inputSchema: ReadFileInput::class, 21 | description: 'Read the contents of a file at the given path.', 22 | ); 23 | } 24 | 25 | public function execute(object $input): string 26 | { 27 | if ($input->path === 'file1.php') { 28 | return json_encode([ 29 | 'content' => <<<'PHP' 30 | class ReviewTool extends \App\Domain\Tool\Tool 31 | { 32 | public function __construct() 33 | { 34 | parent::__construct( 35 | name: 'review' 36 | inputSchema: ReviewInput::class, 37 | description: 'Submit a code review for a file at the given path.', 38 | ); 39 | } 40 | 41 | public function getLanguage(): \App\Domain\Tool\ToolLanguage 42 | { 43 | return \App\Domain\Tool\ToolLanguage::PHP; 44 | } 45 | 46 | public function execute(object $input): string 47 | { 48 | // Implementation to submit code review 49 | return json_encode(['status' => 'success', 'message' => 'Code review submitted']); 50 | } 51 | } 52 | PHP, 53 | ]); 54 | } 55 | 56 | if ($input->path === 'file2.php') { 57 | return json_encode([ 58 | 'content' => <<<'PHP' 59 | class ReadFileTool extends \App\Domain\Tool\Tool 60 | { 61 | public function __construct() 62 | { 63 | parent::__construct( 64 | name: 'read_file', 65 | inputSchema: ReadFileInput::class, 66 | description: 'Read the contents of a file at the given path.', 67 | ); 68 | } 69 | 70 | public function getLanguage(): \App\Domain\Tool\ToolLanguage 71 | { 72 | return \App\Domain\Tool\ToolLanguage:PHP; 73 | } 74 | 75 | public function execute(object $input): string 76 | { 77 | // Implementation to read file contents 78 | return json_encode(['content' => 'File contents here']); 79 | } 80 | } 81 | PHP, 82 | ]); 83 | } 84 | 85 | return 'File not found'; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/Agents/CodeReviewer/ReviewInput.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ReviewTool extends PhpTool 13 | { 14 | public const NAME = 'submit_review'; 15 | 16 | public function __construct() 17 | { 18 | parent::__construct( 19 | name: self::NAME, 20 | inputSchema: ReviewInput::class, 21 | description: 'Submit a code review for a pull request. Call this whenever you need to submit a code review for a pull request.', 22 | ); 23 | } 24 | 25 | public function execute(object $input): string 26 | { 27 | // Implementation to submit code review 28 | return json_encode(['status' => 'OK']); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/Agents/Delivery/DeliveryAgent.php: -------------------------------------------------------------------------------- 1 | addMetadata( 39 | // Memory 40 | new SolutionMetadata( 41 | type: MetadataType::Memory, 42 | key: 'order_tip', 43 | content: 'First, retrieve the customer profile to provide personalized service.', 44 | ), 45 | new SolutionMetadata( 46 | type: MetadataType::Memory, 47 | key: 'order_tip_server', 48 | content: 'Always check the server [google.com] status before providing any information.', 49 | ), 50 | new SolutionMetadata( 51 | type: MetadataType::Memory, 52 | key: 'order_tip_repeat', 53 | content: 'Don\'t repeat the same information to the customer. If you have already provided the order number, don\'t repeat it. Provide only new information.', 54 | ), 55 | new SolutionMetadata( 56 | type: MetadataType::Memory, 57 | key: 'order_tip_age', 58 | content: 'Tone of conversation is important, pay attention on age and fit the conversation to the age of the customer.', 59 | ), 60 | 61 | // Prompt 62 | new SolutionMetadata( 63 | type: MetadataType::Prompt, 64 | key: 'server_status', 65 | content: 'Check the server [google.com] status to ensure that the system is operational.', 66 | ), 67 | new SolutionMetadata( 68 | type: MetadataType::Prompt, 69 | key: 'what_is_order_number', 70 | content: 'What is my order number?', 71 | ), 72 | new SolutionMetadata( 73 | type: MetadataType::Prompt, 74 | key: 'when_is_delivery', 75 | content: 'When will my order be delivered?', 76 | ), 77 | new SolutionMetadata( 78 | type: MetadataType::Prompt, 79 | key: 'my_profile', 80 | content: 'Can you tell me more about my profile?', 81 | ), 82 | ); 83 | 84 | // Add a model to the agent 85 | $model = new Model(model: OpenAIModel::Gpt4oMini->value); 86 | $aggregate->addAssociation($model); 87 | 88 | $aggregate->addAssociation(new ToolLink(name: GetDeliveryDateTool::NAME)); 89 | $aggregate->addAssociation(new ToolLink(name: GetOrderNumberTool::NAME)); 90 | $aggregate->addAssociation(new ToolLink(name: GetProfileTool::NAME)); 91 | 92 | $aggregate->addAssociation(new ToolLink(name: AskAgentTool::NAME)); 93 | $aggregate->addAssociation( 94 | new AgentLink( 95 | name: SiteStatusCheckerAgent::NAME, 96 | outputSchema: StatusCheckOutput::class, 97 | ), 98 | ); 99 | 100 | return $aggregate; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/Agents/Delivery/DeliveryAgentFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class GetDeliveryDateTool extends PhpTool 14 | { 15 | public const NAME = 'get_delivery_date'; 16 | 17 | public function __construct() 18 | { 19 | parent::__construct( 20 | name: self::NAME, 21 | inputSchema: DeliveryDateInput::class, 22 | description: 'Get the delivery date for a customer\'s order. Call this whenever you need to know the delivery date, for example when a customer asks \'Where is my package\'', 23 | ); 24 | } 25 | 26 | public function execute(object $input): string 27 | { 28 | return \json_encode([ 29 | 'delivery_date' => Carbon::now()->addDays(\rand(1, 100))->toDateString(), 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/Agents/Delivery/GetOrderNumberTool.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class GetOrderNumberTool extends PhpTool 13 | { 14 | public const NAME = 'get_order_number'; 15 | 16 | public function __construct() 17 | { 18 | parent::__construct( 19 | name: self::NAME, 20 | inputSchema: OrderNumberInput::class, 21 | description: 'Get the order number for a customer.', 22 | ); 23 | } 24 | 25 | public function execute(object $input): string 26 | { 27 | return \json_encode([ 28 | 'customer_id' => $input->customerId, 29 | 'customer' => 'John Doe', 30 | 'order_number' => 'abc-' . $input->customerId, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/Agents/Delivery/GetProfileTool.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class GetProfileTool extends PhpTool 13 | { 14 | public const NAME = 'get_profile'; 15 | 16 | public function __construct() 17 | { 18 | parent::__construct( 19 | name: self::NAME, 20 | inputSchema: ProfileInput::class, 21 | description: 'Get the customer\'s profile information.', 22 | ); 23 | } 24 | 25 | public function execute(object $input): string 26 | { 27 | return \json_encode([ 28 | 'account_uuid' => $input->accountId, 29 | 'first_name' => 'John', 30 | 'last_name' => 'Doe', 31 | 'age' => \rand(10, 100), 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/Agents/Delivery/OrderNumberInput.php: -------------------------------------------------------------------------------- 1 | getCurrentMemory($sessionUuid); 22 | 23 | $memories->addMemory($metadata); 24 | 25 | $this->cache->set($this->getKey($sessionUuid), $memories); 26 | } 27 | 28 | public function updateMemory(Uuid $sessionUuid, SolutionMetadata $metadata): void 29 | { 30 | $memories = $this->getCurrentMemory($sessionUuid); 31 | $memories->updateMemory($metadata); 32 | 33 | $this->cache->set($this->getKey($sessionUuid), $memories); 34 | } 35 | 36 | public function getCurrentMemory(Uuid $sessionUuid): Memories 37 | { 38 | return $this->cache->get($this->getKey($sessionUuid)) ?? new Memories( 39 | \Ramsey\Uuid\Uuid::uuid4(), 40 | ); 41 | } 42 | 43 | private function getKey(Uuid $sessionUuid): string 44 | { 45 | return 'user_memory'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/Agents/DynamicMemoryTool/DynamicMemoryTool.php: -------------------------------------------------------------------------------- 1 | preference, 32 | ); 33 | 34 | $sessionUuid = Uuid::fromString($input->sessionId); 35 | $this->memoryService->addMemory($sessionUuid, $metadata); 36 | 37 | return 'Memory updated'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/Agents/DynamicMemoryTool/Memories.php: -------------------------------------------------------------------------------- 1 | */ 16 | public array $memories = [], 17 | ) {} 18 | 19 | public function addMemory(SolutionMetadata $metadata): void 20 | { 21 | $this->memories[] = $metadata; 22 | } 23 | 24 | public function getIterator(): Traversable 25 | { 26 | yield from $this->memories; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/Agents/TaskSplitter/GetProjectDescription.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class GetProjectDescription extends PhpTool 13 | { 14 | public const NAME = 'get_project_description'; 15 | 16 | public function __construct() 17 | { 18 | parent::__construct( 19 | name: self::NAME, 20 | inputSchema: ProjectDescriptionInput::class, 21 | description: 'Get the description of a project from the project management system.', 22 | ); 23 | } 24 | 25 | public function execute(object $input): string 26 | { 27 | return json_encode([ 28 | 'uuid' => $input->uuid, 29 | 'title' => 'Оплата услуг клиентом', 30 | 'description' => <<<'TEXT' 31 | **As a customer, I want to be able to:** 32 | 33 | - **Choose a subscription plan during registration:** 34 | - When creating an account on the service's website, I should be offered a choice of different subscription plans. 35 | - Each plan should include a clear description of the services provided and their costs. 36 | 37 | - **Subscribe to a plan using a credit card:** 38 | - After selecting a plan, I need to provide my credit card details for payment. 39 | - There should be an option to save my card details for automatic monthly payments. 40 | 41 | - **Receive monthly payment notifications:** 42 | - I expect to receive notifications via email or through my personal account about upcoming subscription charges. 43 | - The notification should arrive a few days before the charge, giving me enough time to ensure sufficient funds are available. 44 | 45 | - **Access all necessary documents in my personal account:** 46 | - All financial documents, such as receipts and invoices, should be available for download at any time in my personal account. 47 | 48 | - **Cancel the service if needed:** 49 | - I should be able to easily cancel my subscription through my personal account without needing to make additional calls or contact customer support. 50 | 51 | - **Add a new card if the current one expires:** 52 | - If my credit card expires, I want to easily add a new card to the system through my personal account. 53 | 54 | - **Continue using the service after cancellation until the end of the paid period:** 55 | - If I cancel my subscription, I expect to continue using the service until the end of the already paid period. 56 | TEXT 57 | , 58 | ]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/Agents/TaskSplitter/ProjectDescriptionInput.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class TaskCreateTool extends PhpTool 16 | { 17 | public const NAME = 'create_task'; 18 | 19 | public function __construct( 20 | private readonly FilesInterface $files, 21 | private readonly DirectoriesInterface $dirs, 22 | ) { 23 | parent::__construct( 24 | name: self::NAME, 25 | inputSchema: TaskCreateInput::class, 26 | description: 'Create a task or subtask in the task management system.', 27 | ); 28 | } 29 | 30 | public function execute(object $input): string 31 | { 32 | $uuid = (string) Uuid::generate(); 33 | $dir = $this->dirs->get('runtime') . 'tasks/'; 34 | 35 | if ($input->parentTaskUuid !== '') { 36 | $path = \sprintf('%s/%s/%s.json', $input->projectUuid, $input->parentTaskUuid, $uuid); 37 | } else { 38 | $path = \sprintf('%s/%s.json', $input->projectUuid, $uuid); 39 | } 40 | 41 | $fullPath = $dir . $path; 42 | 43 | $this->files->ensureDirectory(\dirname($fullPath)); 44 | $this->files->touch($fullPath); 45 | 46 | $this->files->write( 47 | $fullPath, 48 | \sprintf( 49 | <<<'CONTENT' 50 | uuid: %s 51 | parent_uuid: %s 52 | project_uuid: %s 53 | 54 | --- 55 | 56 | ## %s 57 | 58 | %s 59 | CONTENT, 60 | $uuid, 61 | $input->parentTaskUuid ?? '-', 62 | $input->projectUuid, 63 | $input->name, 64 | \trim($input->description), 65 | ), 66 | ); 67 | 68 | return json_encode(['task_uuid' => $uuid]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/Agents/TaskSplitter/TaskSplitterAgent.php: -------------------------------------------------------------------------------- 1 | addMetadata( 37 | new SolutionMetadata( 38 | type: MetadataType::Memory, 39 | key: 'task_organization_tip', 40 | content: 'Always aim to create a logical hierarchy of tasks and subtasks. Main tasks should be broad objectives, while subtasks should be specific, actionable items.', 41 | ), 42 | new SolutionMetadata( 43 | type: MetadataType::Memory, 44 | key: 'efficiency_tip', 45 | content: 'Try to keep the number of main tasks between 3 and 7 for better manageability. Break down complex tasks into subtasks when necessary.', 46 | ), 47 | 48 | // Prompts examples 49 | new SolutionMetadata( 50 | type: MetadataType::Prompt, 51 | key: 'split_task', 52 | content: 'Split the project description f47ac10b-58cc-4372-a567-0e02b2c3d479 into a list of tasks and subtasks.', 53 | ), 54 | new SolutionMetadata( 55 | type: MetadataType::Configuration, 56 | key: Option::MaxTokens->value, 57 | content: 3000, 58 | ), 59 | ); 60 | 61 | $model = new Model(model: OpenAIModel::Gpt4oMini->value); 62 | $aggregate->addAssociation($model); 63 | 64 | $aggregate->addAssociation(new ToolLink(name: TaskCreateTool::NAME)); 65 | $aggregate->addAssociation(new ToolLink(name: GetProjectDescription::NAME)); 66 | 67 | return $aggregate; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/Agents/TaskSplitter/TaskSplitterAgentFactory.php: -------------------------------------------------------------------------------- 1 | isInstantiable()) { 25 | return; 26 | } 27 | 28 | /** @var AgentFactoryInterface $factory */ 29 | $factory = $this->container->make($class->getName()); 30 | $this->agents->register($factory->create()); 31 | } 32 | 33 | public function finalize(): void 34 | { 35 | // TODO: Implement finalize() method. 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/Application/Assert.php: -------------------------------------------------------------------------------- 1 | ToolRegistry::class, 37 | ToolRegistryInterface::class => ToolRegistry::class, 38 | ToolRepositoryInterface::class => ToolRegistry::class, 39 | 40 | AgentRegistry::class => AgentRegistry::class, 41 | AgentRegistryInterface::class => AgentRegistry::class, 42 | AgentRepositoryInterface::class => AgentRegistry::class, 43 | 44 | OptionsFactoryInterface::class => OptionsFactory::class, 45 | ContextFactoryInterface::class => ContextFactory::class, 46 | 47 | SchemaMapperInterface::class => SchemaMapper::class, 48 | 49 | ExecutorInterface::class => static function ( 50 | ExecutorPipeline $pipeline, 51 | 52 | // Interceptors 53 | GeneratePromptInterceptor $generatePrompt, 54 | InjectModelInterceptor $injectModel, 55 | InjectToolsInterceptor $injectTools, 56 | InjectOptionsInterceptor $injectOptions, 57 | InjectResponseIntoPromptInterceptor $injectResponseIntoPrompt, 58 | ) { 59 | return $pipeline->withInterceptor( 60 | $generatePrompt, 61 | $injectModel, 62 | $injectTools, 63 | $injectOptions, 64 | $injectResponseIntoPrompt, 65 | ); 66 | }, 67 | ]; 68 | } 69 | 70 | public function init( 71 | TokenizerListenerRegistryInterface $listenerRegistry, 72 | ToolsLocator $toolsLocator, 73 | AgentsLocator $agentsLocator, 74 | ): void { 75 | $listenerRegistry->addListener($agentsLocator); 76 | $listenerRegistry->addListener($toolsLocator); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/Application/Bootloader/AgentsChatBootloader.php: -------------------------------------------------------------------------------- 1 | SimpleChatService::class, 26 | ChatHistoryRepositoryInterface::class => static fn( 27 | CacheStorageProviderInterface $cache, 28 | ): ChatHistoryRepositoryInterface => new ChatHistoryRepository( 29 | $cache->storage('chat-messages'), 30 | ), 31 | 32 | PromptGeneratorPipeline::class => static function ( 33 | LinkedAgentsInjector $linkedAgentsInjector, 34 | ): PromptGeneratorPipeline { 35 | $pipeline = new PromptGeneratorPipeline(); 36 | 37 | return $pipeline->withInterceptor( 38 | new InstructionGenerator(), 39 | new AgentMemoryInjector(), 40 | $linkedAgentsInjector, 41 | new SessionContextInjector(), 42 | new UserPromptInjector(), 43 | ); 44 | }, 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/Application/Bootloader/AppBootloader.php: -------------------------------------------------------------------------------- 1 | EnvSuppressErrors::class, 48 | RendererInterface::class => PlainRenderer::class, 49 | ]; 50 | } 51 | 52 | public function init(AbstractKernel $kernel): void 53 | { 54 | // Register the console renderer, that will be used when the application 55 | // is running in the console. 56 | $this->handler->addRenderer(new ConsoleRenderer()); 57 | 58 | $kernel->running(function (): void { 59 | // Register the JSON renderer, that will be used when the application is 60 | // running in the HTTP context and a JSON response is expected. 61 | $this->handler->addRenderer(new JsonRenderer()); 62 | }); 63 | } 64 | 65 | public function boot(LoggerReporter $logger, FileReporter $files): void 66 | { 67 | // Register the logger reporter, that will be used to log the exceptions using 68 | // the logger component. 69 | $this->handler->addReporter($logger); 70 | 71 | // Register the file reporter. It allows you to save detailed information about an exception to a file 72 | // known as snapshot. 73 | $this->handler->addReporter($files); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/Application/Bootloader/Infrastructure/RoadRunnerBootloader.php: -------------------------------------------------------------------------------- 1 | static fn( 21 | ORMInterface $orm, 22 | ) => new SessionRepository(new Select(orm: $orm, role: SessionRepository::ROLE)), 23 | 24 | EntityManagerInterface::class => SessionEntityManager::class, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/Application/Bootloader/SmartHomeBootloader.php: -------------------------------------------------------------------------------- 1 | static function ( 28 | CacheStorageProviderInterface $provider, 29 | ): DeviceStateManager { 30 | return new DeviceStateManager($provider->storage('smart-home')); 31 | }, 32 | 33 | DeviceStateStorageInterface::class => DeviceStateManager::class, 34 | DeviceStateRepositoryInterface::class => DeviceStateManager::class, 35 | 36 | SmartHomeSystem::class => static function ( 37 | DeviceStateStorageInterface $stateStorage, 38 | DeviceStateRepositoryInterface $stateRepository, 39 | ): SmartHomeSystem { 40 | $smartHome = new SmartHomeSystem( 41 | stateStorage: $stateStorage, 42 | stateRepository: $stateRepository, 43 | ); 44 | 45 | // Living Room Devices 46 | $livingRoomAirConditioner = new SmartAppliance( 47 | 'LR_AC_01', 48 | 'Living Room Air Conditioner', 49 | 'living_room', 50 | 'air_conditioner', 51 | [ 52 | 'temperature' => 0, 53 | 'mode' => 'cool', 54 | ], 55 | ); 56 | $livingRoomMainLight = new Light('LR_MAIN_01', 'Living Room Main Light', 'living_room', 'dimmable'); 57 | $livingRoomTableLamp = new Light('LR_LAMP_01', 'Living Room Table Lamp', 'living_room', 'color'); 58 | $livingRoomThermostat = new Thermostat('LR_THERM_01', 'Living Room Thermostat', 'living_room', 24); 59 | $livingRoomTV = new TV('LR_TV_01', 'Living Room TV', 'living_room', 20, 'HDMI 1'); 60 | $livingRoomFireplace = new SmartAppliance( 61 | 'LR_FIRE_01', 62 | 'Living Room Fireplace', 63 | 'living_room', 64 | 'fireplace', 65 | [ 66 | 'temperature' => 0, 67 | ], 68 | ); 69 | $livingRoomSpeaker = new SmartAppliance( 70 | 'LR_SPEAK_01', 71 | 'Living Room Smart Speaker', 72 | 'living_room', 73 | 'speaker', 74 | [ 75 | 'volume' => 0, 76 | 'radio_station' => 'Classical FM', 77 | ], 78 | ); 79 | 80 | // Kitchen Devices 81 | $kitchenMainLight = new Light('KT_MAIN_01', 'Kitchen Main Light', 'kitchen', 'dimmable'); 82 | $kitchenPendantLights = new Light('KT_PEND_01', 'Kitchen Pendant Lights', 'kitchen', 'dimmable'); 83 | $kitchenRefrigerator = new SmartAppliance( 84 | 'KT_FRIDGE_01', 85 | 'Smart Refrigerator', 86 | 'kitchen', 87 | 'refrigerator', 88 | [ 89 | 'temperature' => 37, 90 | 'mode' => 'normal', 91 | ], 92 | ); 93 | $kitchenOven = new SmartAppliance('KT_OVEN_01', 'Smart Oven', 'kitchen', 'oven'); 94 | $kitchenCoffeeMaker = new SmartAppliance( 95 | 'KT_COFFEE_01', 'Smart Coffee Maker', 'kitchen', 'coffee_maker', 96 | ); 97 | 98 | // Bedroom Devices 99 | $bedroomMainLight = new Light('BR_MAIN_01', 'Bedroom Main Light', 'bedroom', 'dimmable'); 100 | $bedroomNightstandLeft = new Light('BR_NIGHT_L_01', 'Left Nightstand Lamp', 'bedroom', 'color'); 101 | $bedroomNightstandRight = new Light('BR_NIGHT_R_01', 'Right Nightstand Lamp', 'bedroom', 'color'); 102 | $bedroomThermostat = new Thermostat('BR_THERM_01', 'Bedroom Thermostat', 'bedroom', 68); 103 | $bedroomTV = new TV('BR_TV_01', 'Bedroom TV', 'bedroom', 15, 'HDMI 1'); 104 | $bedroomCeilingFan = new SmartAppliance('BR_FAN_01', 'Bedroom Ceiling Fan', 'bedroom', 'fan'); 105 | 106 | // Bathroom Devices 107 | $bathroomMainLight = new Light('BA_MAIN_01', 'Bathroom Main Light', 'bathroom', 'dimmable'); 108 | $bathroomMirrorLight = new Light('BA_MIRROR_01', 'Bathroom Mirror Light', 'bathroom', 'color'); 109 | $bathroomExhaustFan = new SmartAppliance('BA_FAN_01', 'Bathroom Exhaust Fan', 'bathroom', 'fan'); 110 | $bathroomSmartScale = new SmartAppliance('BA_SCALE_01', 'Smart Scale', 'bathroom', 'scale'); 111 | 112 | // Add all devices to the smart home system 113 | $smartHome->addDevice($livingRoomAirConditioner); 114 | $smartHome->addDevice($livingRoomMainLight); 115 | $smartHome->addDevice($livingRoomTableLamp); 116 | $smartHome->addDevice($livingRoomThermostat); 117 | $smartHome->addDevice($livingRoomTV); 118 | $smartHome->addDevice($livingRoomFireplace); 119 | $smartHome->addDevice($livingRoomSpeaker); 120 | 121 | $smartHome->addDevice($kitchenMainLight); 122 | $smartHome->addDevice($kitchenPendantLights); 123 | $smartHome->addDevice($kitchenRefrigerator); 124 | $smartHome->addDevice($kitchenOven); 125 | $smartHome->addDevice($kitchenCoffeeMaker); 126 | 127 | $smartHome->addDevice($bedroomMainLight); 128 | $smartHome->addDevice($bedroomNightstandLeft); 129 | $smartHome->addDevice($bedroomNightstandRight); 130 | $smartHome->addDevice($bedroomThermostat); 131 | $smartHome->addDevice($bedroomTV); 132 | $smartHome->addDevice($bedroomCeilingFan); 133 | 134 | $smartHome->addDevice($bathroomMainLight); 135 | $smartHome->addDevice($bathroomMirrorLight); 136 | $smartHome->addDevice($bathroomExhaustFan); 137 | $smartHome->addDevice($bathroomSmartScale); 138 | 139 | return $smartHome; 140 | }, 141 | ]; 142 | } 143 | 144 | public function boot( 145 | AgentRepositoryInterface $agents, 146 | ): void { 147 | /** @var SmartHomeControlAgent $agent */ 148 | // $agent = $agents->get(SmartHomeControlAgent::NAME); 149 | // 150 | // $agent->addAssociation(new ToolLink(name: DynamicMemoryTool::NAME)); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/src/Application/Entity/Json.php: -------------------------------------------------------------------------------- 1 | getMessage(), $e->getCode(), $e); 31 | } 32 | } 33 | 34 | public function jsonSerialize(): array 35 | { 36 | return $this->data instanceof \JsonSerializable 37 | ? $this->data->jsonSerialize() 38 | : $this->data; 39 | } 40 | 41 | public function __toString(): string 42 | { 43 | return \json_encode($this); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/Application/Entity/Uuid.php: -------------------------------------------------------------------------------- 1 | uuid->equals($uuid->uuid); 34 | } 35 | 36 | public function __toString(): string 37 | { 38 | return $this->uuid->toString(); 39 | } 40 | 41 | public function jsonSerialize(): string 42 | { 43 | return $this->uuid->toString(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/Application/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | isInstantiable()) { 25 | return; 26 | } 27 | 28 | $tool = $this->container->make($class->getName()); 29 | $this->tools->register($tool); 30 | } 31 | 32 | public function finalize(): void 33 | { 34 | // TODO: Implement finalize() method. 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/Domain/Chat/EntityManagerInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getIterator(): Traversable 18 | { 19 | return new \ArrayIterator( 20 | $this->toPrompt()->getMessages(), 21 | ); 22 | } 23 | 24 | public function isEmpty(): bool 25 | { 26 | return $this->data === []; 27 | } 28 | 29 | public function toPrompt(): Prompt 30 | { 31 | return Prompt::fromArray($this->data); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/Domain/Chat/PromptGenerator/Dump.php: -------------------------------------------------------------------------------- 1 | prompt->format()); 19 | 20 | return $next($input); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/Domain/Chat/PromptGenerator/SessionContextInjector.php: -------------------------------------------------------------------------------- 1 | prompt instanceof Prompt); 22 | 23 | if ( 24 | (!$input->context instanceof Context) 25 | || $input->context->getAuthContext() === null 26 | ) { 27 | return $next($input); 28 | } 29 | 30 | return $next( 31 | input: $input->withPrompt( 32 | $input->prompt 33 | ->withAddedMessage( 34 | MessagePrompt::system( 35 | prompt: 'Session context: {active_context}', 36 | ), 37 | )->withValues( 38 | values: [ 39 | 'active_context' => \json_encode($input->context->getAuthContext()), 40 | ], 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/Domain/Chat/Session.php: -------------------------------------------------------------------------------- 1 | createdAt = $now; 58 | $this->updatedAt = $now; 59 | 60 | $this->updateHistory([]); 61 | } 62 | 63 | public function updateHistory(array $messages): void 64 | { 65 | $this->history = new History($messages); 66 | } 67 | 68 | public function isFinished(): bool 69 | { 70 | return $this->finishedAt !== null; 71 | } 72 | 73 | public function getUuid(): UuidInterface 74 | { 75 | return $this->uuid->uuid; 76 | } 77 | 78 | public function getAgentName(): string 79 | { 80 | return $this->agentName; 81 | } 82 | 83 | public function setDescription(string $description): void 84 | { 85 | $this->title = $description; 86 | } 87 | 88 | public function getDescription(): ?string 89 | { 90 | return $this->title; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/Domain/Chat/SessionRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | sessions->forUpdate()->getByUuid($sessionUuid); 43 | 44 | if ($session->isFinished()) { 45 | throw new ChatNotFoundException('Session is finished'); 46 | } 47 | 48 | return $session; 49 | } 50 | 51 | public function startSession(UuidInterface $accountUuid, string $agentName): UuidInterface 52 | { 53 | if (!$this->agents->has($agentName)) { 54 | throw new AgentNotFoundException($agentName); 55 | } 56 | 57 | $agent = $this->agents->get($agentName); 58 | 59 | $session = new Session( 60 | uuid: Uuid::generate(), 61 | accountUuid: new Uuid($accountUuid), 62 | agentName: $agentName, 63 | ); 64 | 65 | // Set the title of the session to the agent's description. 66 | $session->setDescription($agent->getDescription()); 67 | 68 | $this->updateSession($session); 69 | 70 | return $session->uuid->uuid; 71 | } 72 | 73 | public function ask(UuidInterface $sessionUuid, string|\Stringable $message): UuidInterface 74 | { 75 | $session = $this->getSession($sessionUuid); 76 | 77 | $prompt = null; 78 | if (!$session->history->isEmpty()) { 79 | $prompt = $session->history->toPrompt(); 80 | } 81 | 82 | $messageUuid = Uuid::generate(); 83 | 84 | $this->eventDispatcher->dispatch( 85 | new \LLM\Agents\Chat\Event\Question( 86 | sessionUuid: $session->uuid->uuid, 87 | messageUuid: $messageUuid->uuid, 88 | createdAt: new \DateTimeImmutable(), 89 | message: $message, 90 | ), 91 | ); 92 | 93 | $execution = $this->buildAgent( 94 | session: $session, 95 | prompt: $prompt, 96 | )->ask($message); 97 | 98 | $this->handleResult($execution, $session); 99 | 100 | return $messageUuid->uuid; 101 | } 102 | 103 | public function closeSession(UuidInterface $sessionUuid): void 104 | { 105 | $session = $this->getSession($sessionUuid); 106 | $session->finishedAt = new \DateTimeImmutable(); 107 | 108 | $this->updateSession($session); 109 | } 110 | 111 | public function updateSession(SessionInterface $session): void 112 | { 113 | $this->em->persist($session)->flush(); 114 | } 115 | 116 | private function handleResult(Execution $execution, Session $session): void 117 | { 118 | $finished = false; 119 | while (true) { 120 | $result = $execution->result; 121 | $prompt = $execution->prompt; 122 | 123 | if ($result instanceof ToolCalledResponse) { 124 | // First, call all tools. 125 | $toolsResponse = []; 126 | foreach ($result->tools as $tool) { 127 | $toolsResponse[] = $this->callTool($session, $tool); 128 | } 129 | 130 | // Then add the tools responses to the prompt. 131 | foreach ($toolsResponse as $toolResponse) { 132 | $prompt = $prompt->withAddedMessage($toolResponse); 133 | } 134 | 135 | $execution = $this->buildAgent( 136 | session: $session, 137 | prompt: $prompt, 138 | )->continue(); 139 | } elseif ($result instanceof ChatResponse) { 140 | $finished = true; 141 | 142 | $this->eventDispatcher->dispatch( 143 | new \LLM\Agents\Chat\Event\Message( 144 | sessionUuid: $session->uuid->uuid, 145 | createdAt: new \DateTimeImmutable(), 146 | message: $result->content, 147 | ), 148 | ); 149 | } 150 | 151 | $session->updateHistory($prompt->toArray()); 152 | $this->updateSession($session); 153 | 154 | if ($finished) { 155 | break; 156 | } 157 | } 158 | } 159 | 160 | private function buildAgent(Session $session, ?Prompt $prompt): AgentExecutorBuilder 161 | { 162 | $context = new Context(); 163 | $context->setAuthContext([ 164 | 'account_uuid' => (string) $session->accountUuid, 165 | 'session_uuid' => (string) $session->uuid, 166 | ]); 167 | 168 | $agent = $this->builder 169 | ->withAgentKey($session->agentName) 170 | ->withStreamChunkCallback( 171 | new StreamChunkCallback( 172 | sessionUuid: $session->uuid->uuid, 173 | eventDispatcher: $this->eventDispatcher, 174 | ), 175 | ) 176 | ->withPromptContext($context); 177 | 178 | if ($prompt === null) { 179 | return $agent; 180 | } 181 | 182 | $memories = $this->memoryService->getCurrentMemory($session->uuid); 183 | 184 | 185 | return $agent->withPrompt($prompt->withValues([ 186 | 'dynamic_memory' => \implode( 187 | "\n", 188 | \array_map( 189 | fn(SolutionMetadata $memory) => $memory->content, 190 | $memories->memories, 191 | ), 192 | ), 193 | ])); 194 | } 195 | 196 | private function callTool(Session $session, ToolCall $tool): ToolCallResultMessage 197 | { 198 | $this->eventDispatcher->dispatch( 199 | new \LLM\Agents\Chat\Event\ToolCall( 200 | sessionUuid: $session->uuid->uuid, 201 | id: $tool->id, 202 | tool: $tool->name, 203 | arguments: $tool->arguments, 204 | createdAt: new \DateTimeImmutable(), 205 | ), 206 | ); 207 | 208 | $functionResult = $this->toolExecutor->execute($tool->name, $tool->arguments); 209 | 210 | $this->eventDispatcher->dispatch( 211 | new \LLM\Agents\Chat\Event\ToolCallResult( 212 | sessionUuid: $session->uuid->uuid, 213 | id: $tool->id, 214 | tool: $tool->name, 215 | result: $functionResult, 216 | createdAt: new \DateTimeImmutable(), 217 | ), 218 | ); 219 | 220 | return new ToolCallResultMessage( 221 | id: $tool->id, 222 | content: [$functionResult], 223 | ); 224 | } 225 | 226 | public function getLatestSession(): ?SessionInterface 227 | { 228 | return $this->sessions->findOneLatest(); 229 | } 230 | 231 | public function getLatestSessions(int $limit = 3): array 232 | { 233 | return \iterator_to_array($this->sessions->findAllLatest($limit)); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /app/src/Endpoint/Console/AgentsListCommand.php: -------------------------------------------------------------------------------- 1 | input, $this->output); 22 | 23 | $io->block('Available agents:'); 24 | 25 | $rows = []; 26 | foreach ($agents->all() as $agent) { 27 | $tools = \array_map(static fn(ToolLink $tool): string => '- ' . $tool->getName(), $agent->getTools()); 28 | $rows[] = [ 29 | $agent->getKey() . PHP_EOL . '- ' . $agent->getModel()->name, 30 | \implode(PHP_EOL, $tools), 31 | \wordwrap($agent->getDescription(), 50, "\n", true), 32 | ]; 33 | } 34 | 35 | $io->table(['Agent', 'Tools', 'Description'], $rows); 36 | 37 | return self::SUCCESS; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/Endpoint/Console/ChatCommand.php: -------------------------------------------------------------------------------- 1 | output); 36 | $cursor->clearScreen(); 37 | $console->run(command: 'agent:list', output: $this->output); 38 | 39 | $chat = new ChatSession( 40 | input: $this->input, 41 | output: $this->output, 42 | agents: $agents, 43 | chat: $chat, 44 | chatHistory: $chatHistory, 45 | tools: $tools, 46 | ); 47 | 48 | $chat->run( 49 | accountUuid: Uuid::fromString('00000000-0000-0000-0000-000000000000'), 50 | openLatest: $this->openLatest, 51 | ); 52 | 53 | return self::SUCCESS; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/Endpoint/Console/ChatWindowCommand.php: -------------------------------------------------------------------------------- 1 | input, 30 | output: $this->output, 31 | chatHistory: $chatHistory, 32 | chat: $chatService, 33 | ); 34 | 35 | $chatWindow->run( 36 | sessionUuid: $this->sessionUuid ? Uuid::fromString($this->sessionUuid) : null, 37 | ); 38 | 39 | return self::SUCCESS; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/Endpoint/Console/DisplaySmartHomeStatusCommand.php: -------------------------------------------------------------------------------- 1 | getLastActionTime() === $lastUpdate) { 33 | \sleep(1); 34 | continue; 35 | } 36 | 37 | $lastUpdate = $stateRepository->getLastActionTime(); 38 | 39 | // Clear the console screen 40 | $this->output->write("\033\143"); 41 | 42 | $rooms = $smartHome->getRoomList(); 43 | foreach ($rooms as $room) { 44 | $this->output->writeln("{$room}"); 45 | 46 | $devices = $smartHome->getRoomDevices($room); 47 | $table = new Table($this->output); 48 | $table->setHeaders(['Device', 'Status', 'Details']); 49 | 50 | foreach ($devices as $device) { 51 | $status = $device->getStatus() ? 'ON' : 'OFF'; 52 | $details = $this->formatDetails($device->getDetails()); 53 | $table->addRow([$device->name, $status, $details]); 54 | } 55 | 56 | $table->render(); 57 | $this->output->writeln(''); 58 | } 59 | } 60 | 61 | return self::SUCCESS; 62 | } 63 | 64 | private function formatDetails(array $details): string 65 | { 66 | $formattedDetails = []; 67 | foreach ($details as $key => $value) { 68 | if ($key === 'status') { 69 | continue; 70 | } 71 | $formattedDetails[] = "{$key}: {$value}"; 72 | } 73 | 74 | return \implode(', ', $formattedDetails); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/Endpoint/Console/KnowledgeBaseGenerator.php: -------------------------------------------------------------------------------- 1 | dirs = $dirs; 29 | $this->basePath = $dirs->get('root'); 30 | $this->outputDir = $dirs->get('root') . '/knowledge-base'; 31 | $this->files = $files; 32 | $files->ensureDirectory(directory: $this->outputDir); 33 | 34 | // Domain Layer 35 | $this->writeContent( 36 | description: <<<'TEXT' 37 | This is an abstraction layer for the LLM Agents. It can be used as a template for creating new agents. 38 | TEXT, 39 | sourcePath: [ 40 | $dirs->get('root') . 'vendor/llm-agents/agents/src', 41 | ], 42 | outputPath: $this->outputDir . '/domain-layer.txt', 43 | ); 44 | 45 | // Packages 46 | $this->writeContent( 47 | description: <<<'TEXT' 48 | This is an abstraction layer for the LLM Agents. It can be used as a template for creating new agents. 49 | TEXT, 50 | sourcePath: [ 51 | $dirs->get('root') . 'vendor/llm-agents/prompt-generator/src', 52 | $dirs->get('root') . 'vendor/llm-agents/openai-client/src', 53 | $dirs->get('root') . 'vendor/llm-agents/json-schema-mapper/src', 54 | ], 55 | outputPath: $this->outputDir . '/packages-layer.txt', 56 | ); 57 | 58 | // Cli-chat 59 | $this->writeContent( 60 | description: <<<'TEXT' 61 | This is an abstraction layer for the LLM Agents. It can be used as a template for creating new agents. 62 | TEXT, 63 | sourcePath: [ 64 | $dirs->get('root') . 'vendor/llm-agents/cli-chat/src', 65 | ], 66 | outputPath: $this->outputDir . '/cli-chat.txt', 67 | ); 68 | 69 | // Agents Example 70 | $this->writeContent( 71 | description: <<<'TEXT' 72 | This is an example of Agent with Tools, Memory and Initial Prompts. It can be used as a template for creating new agents and tools. 73 | Important: Always use gp4-0-mini as the base model for new agents. 74 | TEXT, 75 | sourcePath: [ 76 | $dirs->get('app') . 'src/Agents', 77 | $dirs->get('root') . 'vendor/llm-agents/agent-site-status-checker/src', 78 | $dirs->get('root') . 'vendor/llm-agents/agent-symfony-console/src', 79 | ], 80 | outputPath: $this->outputDir . '/agents-example.txt', 81 | ); 82 | 83 | // Application example 84 | $this->writeContent( 85 | description: <<<'TEXT' 86 | This is an example of Application Layer. It can be used as a template for creating new applications. 87 | TEXT, 88 | sourcePath: [ 89 | $dirs->get('app') . 'src/Application', 90 | $dirs->get('app') . 'src/Domain', 91 | $dirs->get('app') . 'src/Infrastructure', 92 | ], 93 | outputPath: $this->outputDir . '/app-example.txt', 94 | ); 95 | 96 | return self::SUCCESS; 97 | } 98 | 99 | private function writeContent( 100 | string $description, 101 | string|array $sourcePath, 102 | string $outputPath, 103 | ): void { 104 | $found = Finder::create()->name('*.php')->in($sourcePath); 105 | 106 | $description .= PHP_EOL; 107 | 108 | foreach ($found as $file) { 109 | $description .= '//' . \trim(\str_replace($this->basePath, '', $file->getPath())) . PHP_EOL; 110 | $description .= \str_replace(['getContents()) . PHP_EOL; 111 | } 112 | 113 | $description = \preg_replace('/^\s*[\r\n]+/m', '', $description); 114 | 115 | $this->info('Writing ' . $outputPath); 116 | $this->files->write($outputPath, $description); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/Endpoint/Console/ToolListCommand.php: -------------------------------------------------------------------------------- 1 | input, $this->output); 21 | 22 | $io->block('Available tools:'); 23 | 24 | $rows = []; 25 | foreach ($tools->all() as $tool) { 26 | $rows[] = [$tool->getName(), $tool->getDescription()]; 27 | } 28 | 29 | $io->table(['Tool', 'Description'], $rows); 30 | 31 | return self::SUCCESS; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/Infrastructure/CycleOrm/Entity/SessionEntityManager.php: -------------------------------------------------------------------------------- 1 | em->persist($entity); 20 | } 21 | 22 | return $this; 23 | } 24 | 25 | public function delete(SessionInterface ...$entities): self 26 | { 27 | foreach ($entities as $entity) { 28 | $this->em->delete($entity); 29 | } 30 | 31 | return $this; 32 | } 33 | 34 | public function flush(): void 35 | { 36 | $this->em->run(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/Infrastructure/CycleOrm/Repository/SessionRepository.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class SessionRepository extends Repository implements SessionRepositoryInterface 18 | { 19 | public const ROLE = Session::ROLE; 20 | 21 | public function findByUuid(UuidInterface $uuid): ?SessionInterface 22 | { 23 | return $this->findByPK($uuid); 24 | } 25 | 26 | public function getByUuid(UuidInterface $uuid): SessionInterface 27 | { 28 | $session = $this->findByUuid($uuid); 29 | 30 | if ($session === null) { 31 | throw new SessionNotFoundException(\sprintf('Session with UUID %s not found', $uuid)); 32 | } 33 | 34 | return $session; 35 | } 36 | 37 | public function findOneLatest(): ?SessionInterface 38 | { 39 | return $this->select()->orderBy('createdAt', 'DESC')->fetchOne(); 40 | } 41 | 42 | public function findAllLatest(int $limit = 3): iterable 43 | { 44 | return $this->select()->orderBy('createdAt', 'DESC')->limit($limit)->fetchAll(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/Infrastructure/CycleOrm/Table/SessionTable.php: -------------------------------------------------------------------------------- 1 | history->addMessage($message->sessionUuid, $message); 24 | } 25 | 26 | #[Listener] 27 | public function listenMessage(Message $message): void 28 | { 29 | $this->history->addMessage($message->sessionUuid, $message); 30 | } 31 | 32 | #[Listener] 33 | public function listenQuestion(Question $message): void 34 | { 35 | $this->history->addMessage($message->sessionUuid, $message); 36 | } 37 | 38 | #[Listener] 39 | public function listenToolCallResult(ToolCallResult $message): void 40 | { 41 | $this->history->addMessage($message->sessionUuid, $message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/Infrastructure/RoadRunner/Chat/ChatHistoryRepository.php: -------------------------------------------------------------------------------- 1 | cache->get((string) $sessionUuid); 20 | 21 | foreach ($messages as $message) { 22 | yield $message; 23 | } 24 | } 25 | 26 | public function addMessage(UuidInterface $sessionUuid, object $message): void 27 | { 28 | $messages = (array) $this->cache->get((string) $sessionUuid); 29 | $messages[] = $message; 30 | 31 | $this->cache->set((string) $sessionUuid, $messages); 32 | } 33 | 34 | public function clear(UuidInterface $sessionUuid): void 35 | { 36 | $this->cache->delete((string) $sessionUuid); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/Infrastructure/RoadRunner/SmartHome/DeviceStateManager.php: -------------------------------------------------------------------------------- 1 | cache->get('device_' . $id); 28 | } 29 | 30 | public function update(SmartDevice $device): void 31 | { 32 | $this->cache->set( 33 | 'device_' . $device->id, 34 | $device, 35 | CarbonInterval::seconds(self::DEVICE_STATE_TTL), 36 | ); 37 | 38 | $this->cache->set(self::LAST_ACTION_KEY, \time(), CarbonInterval::seconds(self::LAST_ACTION_TTL)); 39 | } 40 | 41 | public function getLastActionTime(): ?int 42 | { 43 | return $this->cache->get(self::LAST_ACTION_KEY) ?? null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llm-agents/app", 3 | "type": "project", 4 | "license": "MIT", 5 | "description": "Lang chain service", 6 | "require": { 7 | "php": ">=8.2", 8 | "ext-mbstring": "*", 9 | "ext-sockets": "*", 10 | "cycle/entity-behavior": "^1.2", 11 | "guzzlehttp/guzzle": "^7.0", 12 | "internal/dload": "^0.2.2", 13 | "llm-agents/agent-site-status-checker": "^1.1", 14 | "llm-agents/agent-smart-home-control": "^1.1", 15 | "llm-agents/agent-symfony-console": "^1.1", 16 | "llm-agents/agents": "^1.5", 17 | "llm-agents/cli-chat": "^1.4", 18 | "llm-agents/json-schema-mapper": "^1.0", 19 | "llm-agents/openai-client": "^1.3", 20 | "llm-agents/prompt-generator": "^1.2", 21 | "nesbot/carbon": "^3.4", 22 | "nyholm/psr7": "^1.8", 23 | "openai-php/client": "^0.10.1", 24 | "spiral-packages/league-event": "^1.0", 25 | "spiral/cycle-bridge": "^2.9", 26 | "spiral/framework": "^3.14", 27 | "spiral/roadrunner-bridge": "^4.0" 28 | }, 29 | "require-dev": { 30 | "buggregator/trap": "^1.7", 31 | "vimeo/psalm": "^5.9", 32 | "spiral/roadrunner-cli": "^2.6", 33 | "spiral/testing": "^2.3" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "App\\": "app/src", 38 | "Database\\": "app/database" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Tests\\": "tests" 44 | } 45 | }, 46 | "extra": { 47 | "publish-cmd": "php app.php publish" 48 | }, 49 | "config": { 50 | "sort-packages": true, 51 | "allow-plugins": { 52 | "spiral/composer-publish-plugin": true, 53 | "php-http/discovery": true 54 | } 55 | }, 56 | "scripts": { 57 | "post-create-project-cmd": [ 58 | "php app.php encrypt:key -m .env", 59 | "php app.php configure --quiet", 60 | "rr get-binary --quiet", 61 | "composer dump-autoload" 62 | ], 63 | "rr:download": "rr get-binary", 64 | "rr:download-protoc": "rr download-protoc-binary", 65 | "test": "vendor/bin/phpunit", 66 | "test-coverage": "vendor/bin/phpunit --coverage", 67 | "psalm:config": "psalm" 68 | }, 69 | "minimum-stability": "dev", 70 | "prefer-stable": true 71 | } 72 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | app: 5 | image: ghcr.io/llm-agents-php/sample-app:1.0.0 6 | environment: 7 | OPENAI_KEY: ${OPENAI_KEY} 8 | -------------------------------------------------------------------------------- /knowledge-base/cli-chat.txt: -------------------------------------------------------------------------------- 1 | This is an abstraction layer for the LLM Agents. It can be used as a template for creating new agents. 2 | //vendor/llm-agents/cli-chat/src 3 | namespace LLM\Agents\Chat; 4 | use Ramsey\Uuid\UuidInterface; 5 | interface SessionInterface 6 | { 7 | public function getUuid(): UuidInterface; 8 | public function getAgentName(): string; 9 | public function updateHistory(array $messages): void; 10 | public function isFinished(): bool; 11 | } 12 | //vendor/llm-agents/cli-chat/src 13 | namespace LLM\Agents\Chat; 14 | use LLM\Agents\Agent\Exception\InvalidBuilderStateException; 15 | use LLM\Agents\Agent\Execution; 16 | use LLM\Agents\AgentExecutor\ExecutorInterceptorInterface; 17 | use LLM\Agents\AgentExecutor\ExecutorInterface; 18 | use LLM\Agents\LLM\OptionsFactoryInterface; 19 | use LLM\Agents\LLM\OptionsInterface; 20 | use LLM\Agents\LLM\Prompt\Chat\MessagePrompt; 21 | use LLM\Agents\LLM\Prompt\Chat\Prompt; 22 | use LLM\Agents\LLM\Prompt\Context; 23 | use LLM\Agents\LLM\PromptContextInterface; 24 | use LLM\Agents\OpenAI\Client\Option; 25 | use LLM\Agents\OpenAI\Client\StreamChunkCallbackInterface; 26 | final class AgentExecutorBuilder 27 | { 28 | private ?Prompt $prompt = null; 29 | private ?string $agentKey = null; 30 | private PromptContextInterface $promptContext; 31 | private OptionsInterface $options; 32 | /** @var ExecutorInterceptorInterface[] */ 33 | private array $interceptors = []; 34 | public function __construct( 35 | private readonly ExecutorInterface $executor, 36 | OptionsFactoryInterface $optionsFactory, 37 | ) { 38 | $this->options = $optionsFactory->create(); 39 | $this->promptContext = new Context(); 40 | } 41 | public function withStreamChunkCallback(StreamChunkCallbackInterface $callback): self 42 | { 43 | $self = clone $this; 44 | $self->options = $this->options->with(Option::StreamChunkCallback, $callback); 45 | return $self; 46 | } 47 | public function withPrompt(Prompt $prompt): self 48 | { 49 | $self = clone $this; 50 | $self->prompt = $prompt; 51 | return $self; 52 | } 53 | public function getPrompt(): ?Prompt 54 | { 55 | return $this->prompt; 56 | } 57 | public function withAgentKey(string $agentKey): self 58 | { 59 | $self = clone $this; 60 | $self->agentKey = $agentKey; 61 | return $self; 62 | } 63 | public function withPromptContext(PromptContextInterface $context): self 64 | { 65 | $self = clone $this; 66 | $self->promptContext = $context; 67 | return $self; 68 | } 69 | public function getPromptContext(): PromptContextInterface 70 | { 71 | return $this->promptContext; 72 | } 73 | public function withMessage(MessagePrompt $message): self 74 | { 75 | if ($this->prompt === null) { 76 | throw new InvalidBuilderStateException('Cannot add message without a prompt'); 77 | } 78 | $this->prompt = $this->prompt->withAddedMessage($message); 79 | return $this; 80 | } 81 | public function withInterceptor(ExecutorInterceptorInterface ...$interceptors): self 82 | { 83 | $self = clone $this; 84 | $self->interceptors = \array_merge($this->interceptors, $interceptors); 85 | return $this; 86 | } 87 | public function ask(string|\Stringable $prompt): Execution 88 | { 89 | if ($this->agentKey === null) { 90 | throw new InvalidBuilderStateException('Agent key is required'); 91 | } 92 | if ($this->prompt !== null) { 93 | $prompt = $this->prompt->withAddedMessage( 94 | MessagePrompt::user( 95 | prompt: $prompt, 96 | ), 97 | ); 98 | } 99 | $execution = $this->executor 100 | ->withInterceptor(...$this->interceptors) 101 | ->execute( 102 | agent: $this->agentKey, 103 | prompt: $prompt, 104 | options: $this->options, 105 | promptContext: $this->promptContext, 106 | ); 107 | $this->prompt = $execution->prompt; 108 | return $execution; 109 | } 110 | public function continue(): Execution 111 | { 112 | if ($this->agentKey === null) { 113 | throw new InvalidBuilderStateException('Agent key is required'); 114 | } 115 | $execution = $this->executor 116 | ->withInterceptor(...$this->interceptors) 117 | ->execute( 118 | agent: $this->agentKey, 119 | prompt: $this->prompt, 120 | options: $this->options, 121 | promptContext: $this->promptContext, 122 | ); 123 | $this->prompt = $execution->prompt; 124 | return $execution; 125 | } 126 | public function __clone() 127 | { 128 | $this->prompt = null; 129 | } 130 | } 131 | //vendor/llm-agents/cli-chat/src/Console 132 | namespace LLM\Agents\Chat\Console; 133 | use Symfony\Component\Console\Cursor; 134 | use Symfony\Component\Console\Formatter\OutputFormatterStyle; 135 | use Symfony\Component\Console\Input\InputInterface; 136 | use Symfony\Component\Console\Output\OutputInterface; 137 | use Symfony\Component\Console\Style\SymfonyStyle; 138 | final class ChatStyle extends SymfonyStyle 139 | { 140 | private readonly Cursor $cursor; 141 | public function __construct(InputInterface $input, OutputInterface $output) 142 | { 143 | $formatter = $output->getFormatter(); 144 | $formatter->setStyle('muted', new OutputFormatterStyle('gray')); 145 | $formatter->setStyle('tool_call', new OutputFormatterStyle('white', 'blue', ['bold'])); 146 | $formatter->setStyle('tool_result', new OutputFormatterStyle('white', 'magenta', ['bold'])); 147 | $formatter->setStyle('response', new OutputFormatterStyle('cyan')); 148 | parent::__construct( 149 | $input, 150 | $output, 151 | ); 152 | $this->cursor = new Cursor($output); 153 | } 154 | /** 155 | * Formats a message as a block of text. 156 | */ 157 | public function block( 158 | string|array $messages, 159 | ?string $type = null, 160 | ?string $style = null, 161 | string $prefix = ' ', 162 | bool $padding = false, 163 | bool $escape = true, 164 | ): void { 165 | parent::block($messages, $type, $style, $prefix, $padding, $escape); 166 | $this->cursor->moveUp(); 167 | } 168 | } 169 | //vendor/llm-agents/cli-chat/src/Console 170 | namespace LLM\Agents\Chat\Console; 171 | use LLM\Agents\Agent\AgentInterface; 172 | use LLM\Agents\Agent\AgentRegistryInterface; 173 | use LLM\Agents\Chat\ChatHistoryRepositoryInterface; 174 | use LLM\Agents\Chat\ChatServiceInterface; 175 | use LLM\Agents\Tool\ToolRegistryInterface; 176 | use Ramsey\Uuid\UuidInterface; 177 | use Symfony\Component\Console\Cursor; 178 | use Symfony\Component\Console\Input\InputInterface; 179 | use Symfony\Component\Console\Output\OutputInterface; 180 | final class ChatSession 181 | { 182 | private readonly ChatStyle $io; 183 | private readonly Cursor $cursor; 184 | private bool $firstMessage = true; 185 | private bool $lastMessageCustom = false; 186 | private UuidInterface $sessionUuid; 187 | public function __construct( 188 | InputInterface $input, 189 | OutputInterface $output, 190 | private readonly AgentRegistryInterface $agents, 191 | private readonly ChatServiceInterface $chat, 192 | private readonly ChatHistoryRepositoryInterface $chatHistory, 193 | private readonly ToolRegistryInterface $tools, 194 | ) { 195 | $this->io = new ChatStyle($input, $output); 196 | $this->cursor = new Cursor($output); 197 | } 198 | public function run(UuidInterface $accountUuid, string $binPath = 'app.php'): void 199 | { 200 | $agent = $this->selectAgent(); 201 | $getCommand = $this->getCommand($agent); 202 | $this->sessionUuid = $this->chat->startSession( 203 | accountUuid: $accountUuid, 204 | agentName: $agent->getKey(), 205 | ); 206 | $sessionInfo = []; 207 | if ($this->io->isVerbose()) { 208 | $sessionInfo = [ 209 | \sprintf('Session started with UUID: %s', $this->sessionUuid), 210 | ]; 211 | } 212 | $message = \sprintf('php %s chat:session %s -v', $binPath, $this->sessionUuid); 213 | $this->io->block(\array_merge($sessionInfo, [ 214 | 'Run the following command to see the AI response', 215 | \str_repeat('-', \strlen($message)), 216 | $message, 217 | ]), style: 'info', padding: true); 218 | while (true) { 219 | $message = $getCommand(); 220 | if ($message === 'exit') { 221 | $this->io->info('Goodbye! Closing chat session...'); 222 | $this->chat->closeSession($this->sessionUuid); 223 | $this->chatHistory->clear($this->sessionUuid); 224 | break; 225 | } elseif ($message === 'refresh') { 226 | continue; 227 | } 228 | if (!empty($message)) { 229 | $this->chat->ask($this->sessionUuid, $message); 230 | } else { 231 | $this->io->warning('Message cannot be empty'); 232 | } 233 | } 234 | } 235 | private function selectAgent(): AgentInterface 236 | { 237 | $availableAgents = []; 238 | foreach ($this->agents->all() as $agent) { 239 | $availableAgents[$agent->getKey()] = $agent->getName(); 240 | } 241 | while (true) { 242 | $agentName = $this->io->choice( 243 | 'Hello! Let\'s start a chat session. Please select an agent:', 244 | $availableAgents, 245 | ); 246 | if ($agentName && $this->agents->has($agentName)) { 247 | $this->cursor->moveUp(\count($availableAgents) + 4); 248 | // clears all the output from the current line 249 | $this->cursor->clearOutput(); 250 | $agent = $this->agents->get($agentName); 251 | $this->io->title($agent->getName()); 252 | // split the description into multiple lines by 200 characters 253 | $this->io->block(\wordwrap($agent->getDescription(), 200, "\n", true)); 254 | $rows = []; 255 | foreach ($agent->getTools() as $tool) { 256 | $tool = $this->tools->get($tool->name); 257 | $rows[] = [$tool->name, \wordwrap($tool->description, 70, "\n", true)]; 258 | } 259 | $this->io->table(['Tool', 'Description'], $rows); 260 | break; 261 | } 262 | $this->io->error('Invalid agent'); 263 | } 264 | return $agent; 265 | } 266 | private function getCommand(AgentInterface $agent): callable 267 | { 268 | return function () use ($agent): string|null { 269 | $initialPrompts = ['custom']; 270 | $cursorOffset = $this->lastMessageCustom ? 5 : 4; 271 | $this->lastMessageCustom = false; 272 | foreach ($agent->getPrompts() as $prompt) { 273 | $initialPrompts[] = $prompt->content; 274 | } 275 | $initialPrompts[] = 'reset'; 276 | $initialPrompts[] = 'exit'; 277 | if (!$this->firstMessage) { 278 | $this->cursor->moveUp(\count($initialPrompts) + $cursorOffset); 279 | // clears all the output from the current line 280 | $this->cursor->clearOutput(); 281 | $this->cursor->moveUp(); 282 | } 283 | if ($this->firstMessage) { 284 | $this->firstMessage = false; 285 | } 286 | $initialPrompt = $this->io->choice('Choose a prompt:', $initialPrompts, 'custom'); 287 | if ($initialPrompt === 'custom') { 288 | // Re-enable input echoing in case it was disabled 289 | \shell_exec('stty sane'); 290 | $initialPrompt = $this->io->ask('You'); 291 | $this->lastMessageCustom = true; 292 | } 293 | return $initialPrompt; 294 | }; 295 | } 296 | } 297 | //vendor/llm-agents/cli-chat/src/Console 298 | namespace LLM\Agents\Chat\Console; 299 | use LLM\Agents\Chat\ChatHistoryRepositoryInterface; 300 | use LLM\Agents\Chat\ChatServiceInterface; 301 | use LLM\Agents\Chat\Event\Message; 302 | use LLM\Agents\Chat\Event\MessageChunk; 303 | use LLM\Agents\Chat\Event\Question; 304 | use LLM\Agents\Chat\Event\ToolCall; 305 | use LLM\Agents\Chat\Event\ToolCallResult; 306 | use LLM\Agents\Chat\Exception\ChatNotFoundException; 307 | use LLM\Agents\Chat\Exception\SessionNotFoundException; 308 | use Ramsey\Uuid\UuidInterface; 309 | use Symfony\Component\Console\Input\InputInterface; 310 | use Symfony\Component\Console\Output\OutputInterface; 311 | final class ChatHistory 312 | { 313 | private UuidInterface $sessionUuid; 314 | /** @var array */ 315 | private array $shownMessages = []; 316 | private bool $shouldStop = false; 317 | private string $lastMessage = ''; 318 | private readonly ChatStyle $io; 319 | public function __construct( 320 | InputInterface $input, 321 | OutputInterface $output, 322 | private readonly ChatHistoryRepositoryInterface $chatHistory, 323 | private readonly ChatServiceInterface $chat, 324 | ) { 325 | $this->io = new ChatStyle($input, $output); 326 | } 327 | public function run(UuidInterface $sessionUuid): void 328 | { 329 | $this->sessionUuid = $sessionUuid; 330 | $this->io->write("\033\143"); 331 | $session = $this->chat->getSession($this->sessionUuid); 332 | $this->io->block([ 333 | \sprintf('Connecting to chat session [%s]...', $this->sessionUuid), 334 | \sprintf('Chat session started with agent [%s]. Press Ctrl+C to exit.', $session->getAgentName()), 335 | ], style: 'info', padding: true); 336 | do { 337 | try { 338 | $this->chat->getSession($this->sessionUuid); 339 | } catch (ChatNotFoundException) { 340 | throw new SessionNotFoundException('Session is closed.'); 341 | } 342 | foreach ($this->iterateMessages() as $message) { 343 | if ($message instanceof MessageChunk) { 344 | if ($this->lastMessage === '' && !$message->isLast) { 345 | $this->io->newLine(); 346 | } 347 | $this->lastMessage .= $message->message; 348 | $this->io->write($line = sprintf('<%s>%s', 'response', $message->message)); 349 | \usleep(20_000); 350 | if ($message->isLast) { 351 | if ($this->lastMessage !== '') { 352 | $this->io->newLine(); 353 | } 354 | $this->lastMessage = ''; 355 | } 356 | } elseif ($message instanceof Question) { 357 | $this->io->block( 358 | \sprintf('> User: %s', $message->message), 359 | style: 'question', 360 | padding: true, 361 | ); 362 | } elseif ($message instanceof ToolCall) { 363 | $this->io->block( 364 | \sprintf( 365 | "<-- Let me call [%s] tool", 366 | $message->tool, 367 | ), 368 | style: 'tool_call', 369 | padding: true, 370 | ); 371 | if ($this->io->isVerbose()) { 372 | $this->io->block( 373 | messages: \json_encode(\json_decode($message->arguments, true), \JSON_PRETTY_PRINT), 374 | style: 'muted', 375 | ); 376 | } 377 | } elseif ($message instanceof ToolCallResult) { 378 | $this->io->block( 379 | \sprintf( 380 | "--> [%s]", 381 | $message->tool, 382 | ), 383 | style: 'tool_result', 384 | padding: true, 385 | ); 386 | if ($this->io->isVerbose()) { 387 | // unescape the JSON string 388 | $result = \str_replace('\\"', '"', $message->result); 389 | $this->io->block( 390 | messages: \json_validate($result) 391 | ? \json_encode(\json_decode($result, true), \JSON_PRETTY_PRINT) 392 | : $result, 393 | style: 'muted', 394 | ); 395 | } 396 | } 397 | } 398 | \sleep(2); 399 | } while (!$this->shouldStop); 400 | } 401 | /** 402 | * @return iterable 403 | */ 404 | private function iterateMessages(): iterable 405 | { 406 | $messages = $this->chatHistory->getMessages($this->sessionUuid); 407 | foreach ($messages as $message) { 408 | if ($message instanceof Message || $message instanceof Question || $message instanceof MessageChunk) { 409 | if (\in_array((string) $message->uuid, $this->shownMessages, true)) { 410 | continue; 411 | } 412 | $this->shownMessages[] = (string) $message->uuid; 413 | yield $message; 414 | } elseif ($message instanceof ToolCall) { 415 | if (\in_array($message->id . 'ToolCall', $this->shownMessages, true)) { 416 | continue; 417 | } 418 | $this->shownMessages[] = $message->id . 'ToolCall'; 419 | yield $message; 420 | } elseif ($message instanceof ToolCallResult) { 421 | if (\in_array($message->id . 'ToolCallResult', $this->shownMessages, true)) { 422 | continue; 423 | } 424 | $this->shownMessages[] = $message->id . 'ToolCallResult'; 425 | yield $message; 426 | } 427 | } 428 | } 429 | } 430 | //vendor/llm-agents/cli-chat/src/Event 431 | namespace LLM\Agents\Chat\Event; 432 | use Ramsey\Uuid\Uuid; 433 | use Ramsey\Uuid\UuidInterface; 434 | readonly class Message 435 | { 436 | public UuidInterface $uuid; 437 | public function __construct( 438 | public UuidInterface $sessionUuid, 439 | public \DateTimeImmutable $createdAt, 440 | public string|\Stringable $message, 441 | ) { 442 | $this->uuid = Uuid::uuid4(); 443 | } 444 | } 445 | //vendor/llm-agents/cli-chat/src/Event 446 | namespace LLM\Agents\Chat\Event; 447 | use Ramsey\Uuid\UuidInterface; 448 | final readonly class ToolCall 449 | { 450 | public function __construct( 451 | public UuidInterface $sessionUuid, 452 | public string $id, 453 | public string|\Stringable $tool, 454 | public string $arguments, 455 | public \DateTimeImmutable $createdAt, 456 | ) {} 457 | } 458 | //vendor/llm-agents/cli-chat/src/Event 459 | namespace LLM\Agents\Chat\Event; 460 | use Ramsey\Uuid\UuidInterface; 461 | final readonly class MessageChunk extends Message 462 | { 463 | public function __construct( 464 | UuidInterface $sessionUuid, 465 | \DateTimeImmutable $createdAt, 466 | \Stringable|string $message, 467 | public bool $isLast, 468 | ) { 469 | parent::__construct($sessionUuid, $createdAt, $message); 470 | } 471 | } 472 | //vendor/llm-agents/cli-chat/src/Event 473 | namespace LLM\Agents\Chat\Event; 474 | use Ramsey\Uuid\Uuid; 475 | use Ramsey\Uuid\UuidInterface; 476 | final readonly class Question 477 | { 478 | public UuidInterface $uuid; 479 | public function __construct( 480 | public UuidInterface $sessionUuid, 481 | public UuidInterface $messageUuid, 482 | public \DateTimeImmutable $createdAt, 483 | public string|\Stringable $message, 484 | ) { 485 | $this->uuid = Uuid::uuid4(); 486 | } 487 | } 488 | //vendor/llm-agents/cli-chat/src/Event 489 | namespace LLM\Agents\Chat\Event; 490 | use Ramsey\Uuid\UuidInterface; 491 | final readonly class ToolCallResult 492 | { 493 | public function __construct( 494 | public UuidInterface $sessionUuid, 495 | public string $id, 496 | public string|\Stringable $tool, 497 | public string|\Stringable $result, 498 | public \DateTimeImmutable $createdAt, 499 | ) {} 500 | } 501 | //vendor/llm-agents/cli-chat/src 502 | namespace LLM\Agents\Chat; 503 | enum Role: string 504 | { 505 | case User = 'user'; 506 | case Bot = 'bot'; 507 | case Agent = 'agent'; 508 | case System = 'system'; 509 | case Tool = 'tool'; 510 | } 511 | //vendor/llm-agents/cli-chat/src 512 | namespace LLM\Agents\Chat; 513 | use Ramsey\Uuid\UuidInterface; 514 | interface ChatHistoryRepositoryInterface 515 | { 516 | public function clear(UuidInterface $sessionUuid): void; 517 | public function getMessages(UuidInterface $sessionUuid): iterable; 518 | public function addMessage(UuidInterface $sessionUuid, object $message): void; 519 | } 520 | //vendor/llm-agents/cli-chat/src 521 | namespace LLM\Agents\Chat; 522 | use LLM\Agents\Chat\Event\MessageChunk; 523 | use LLM\Agents\OpenAI\Client\StreamChunkCallbackInterface; 524 | use Psr\EventDispatcher\EventDispatcherInterface; 525 | use Ramsey\Uuid\UuidInterface; 526 | final readonly class StreamChunkCallback implements StreamChunkCallbackInterface 527 | { 528 | public function __construct( 529 | private UuidInterface $sessionUuid, 530 | private ?EventDispatcherInterface $eventDispatcher = null, 531 | ) {} 532 | public function __invoke(?string $chunk, bool $stop, ?string $finishReason = null): void 533 | { 534 | $this->eventDispatcher?->dispatch( 535 | new MessageChunk( 536 | sessionUuid: $this->sessionUuid, 537 | createdAt: new \DateTimeImmutable(), 538 | message: (string) $chunk, 539 | isLast: $stop, 540 | ), 541 | ); 542 | } 543 | } 544 | //vendor/llm-agents/cli-chat/src/Exception 545 | namespace LLM\Agents\Chat\Exception; 546 | class ChatException extends \DomainException 547 | { 548 | } 549 | //vendor/llm-agents/cli-chat/src/Exception 550 | namespace LLM\Agents\Chat\Exception; 551 | final class ChatNotFoundException extends ChatException 552 | { 553 | } 554 | //vendor/llm-agents/cli-chat/src/Exception 555 | namespace LLM\Agents\Chat\Exception; 556 | final class SessionNotFoundException extends ChatException 557 | { 558 | } 559 | //vendor/llm-agents/cli-chat/src 560 | namespace LLM\Agents\Chat; 561 | use LLM\Agents\Chat\Exception\ChatNotFoundException; 562 | use Ramsey\Uuid\UuidInterface; 563 | interface ChatServiceInterface 564 | { 565 | /** 566 | * Get session by UUID. 567 | * 568 | * @throws ChatNotFoundException 569 | */ 570 | public function getSession(UuidInterface $sessionUuid): SessionInterface; 571 | public function updateSession(SessionInterface $session): void; 572 | /** 573 | * Start session on context. 574 | * 575 | * @return UuidInterface Session UUID 576 | */ 577 | public function startSession(UuidInterface $accountUuid, string $agentName): UuidInterface; 578 | /** 579 | * Ask question to chat. 580 | * 581 | * @return UuidInterface Message UUID. 582 | */ 583 | public function ask(UuidInterface $sessionUuid, string|\Stringable $message): UuidInterface; 584 | /** 585 | * Close session. 586 | */ 587 | public function closeSession(UuidInterface $sessionUuid): void; 588 | } 589 | -------------------------------------------------------------------------------- /knowledge-base/mermaid-schema.md: -------------------------------------------------------------------------------- 1 | # Agent Domain Layer Diagram 2 | 3 | This mermaid diagram provides a visual representation of the Agent domain layer, showcasing the main classes, 4 | interfaces, and their relationships. Here's a brief explanation of the diagram: 5 | 6 | AgentInterface is the core interface that defines the contract for agents. 7 | AgentAggregate implements AgentInterface and aggregates an Agent instance along with other Solution objects. 8 | Agent, Model, and ToolLink all inherit from the abstract Solution class. 9 | Solution has a composition relationship with SolutionMetadata. 10 | AgentExecutor is responsible for executing agents and depends on various interfaces and classes. 11 | AgentExecutorBuilder is used to build and configure AgentExecutor instances. 12 | AgentFactoryInterface, AgentRepositoryInterface, and AgentRegistryInterface are interfaces for creating, retrieving, and 13 | registering agents, respectively. 14 | 15 | ```mermaid 16 | classDiagram 17 | class AgentInterface { 18 | <> 19 | +getKey() string 20 | +getName() string 21 | +getDescription() string 22 | +getInstruction() string 23 | +getTools() array 24 | +getModel() Model 25 | +getMemory() array 26 | +getPrompts() array 27 | +getConfiguration() array 28 | } 29 | 30 | class AgentAggregate { 31 | -agent: Agent 32 | -associations: array 33 | +addAssociation(Solution) 34 | +addMetadata(SolutionMetadata) 35 | } 36 | 37 | class Agent { 38 | +key: string 39 | +name: string 40 | +description: string 41 | +instruction: string 42 | +isActive: bool 43 | } 44 | 45 | class Solution { 46 | <> 47 | +uuid: Uuid 48 | +name: string 49 | +type: SolutionType 50 | +description: string 51 | -metadata: array 52 | +addMetadata(SolutionMetadata) 53 | +getMetadata() array 54 | } 55 | 56 | class SolutionMetadata { 57 | +type: MetadataType 58 | +key: string 59 | +content: string|Stringable|int 60 | } 61 | 62 | class Model { 63 | +model: OpenAIModel 64 | } 65 | 66 | class ToolLink { 67 | +getName() string 68 | } 69 | 70 | class AgentExecutor { 71 | -llm: PipelineInterface 72 | -promptGenerator: AgentPromptGenerator 73 | -tools: ToolRepositoryInterface 74 | -agents: AgentRepositoryInterface 75 | -schemaMapper: SchemaMapper 76 | +execute(string, string|Stringable|ChatPrompt, Context, MessageCallback, array) Execution 77 | } 78 | 79 | class AgentExecutorBuilder { 80 | -executor: AgentExecutor 81 | -prompt: ChatPrompt 82 | -agentKey: string 83 | -sessionContext: array 84 | -callback: MessageCallback 85 | +withCallback(MessageCallback) self 86 | +withPrompt(ChatPrompt) self 87 | +withAgentKey(string) self 88 | +withSessionContext(array) self 89 | +withMessage(MessagePrompt) self 90 | +ask(string|Stringable) Execution 91 | +continue() Execution 92 | } 93 | 94 | class AgentFactoryInterface { 95 | <> 96 | +create() AgentInterface 97 | } 98 | 99 | class AgentRepositoryInterface { 100 | <> 101 | +get(string) AgentInterface 102 | +has(string) bool 103 | } 104 | 105 | class AgentRegistryInterface { 106 | <> 107 | +register(AgentInterface) 108 | +all() iterable 109 | } 110 | 111 | AgentAggregate ..|> AgentInterface 112 | AgentAggregate o-- Agent 113 | AgentAggregate o-- Solution 114 | Agent --|> Solution 115 | Model --|> Solution 116 | ToolLink --|> Solution 117 | AgentExecutor --> AgentRepositoryInterface 118 | AgentExecutor --> ToolRepositoryInterface 119 | AgentExecutorBuilder o-- AgentExecutor 120 | AgentFactoryInterface ..> AgentInterface 121 | AgentRegistryInterface ..> AgentInterface 122 | AgentRepositoryInterface ..> AgentInterface 123 | Solution o-- SolutionMetadata 124 | ``` 125 | -------------------------------------------------------------------------------- /knowledge-base/packages-layer.txt: -------------------------------------------------------------------------------- 1 | This is an abstraction layer for the LLM Agents. It can be used as a template for creating new agents. 2 | //vendor/llm-agents/prompt-generator/src 3 | namespace LLM\Agents\PromptGenerator; 4 | use LLM\Agents\LLM\AgentPromptGeneratorInterface; 5 | interface PromptGeneratorPipelineInterface extends AgentPromptGeneratorInterface 6 | { 7 | public function withInterceptor(PromptInterceptorInterface ...$interceptor): self; 8 | } 9 | //vendor/llm-agents/prompt-generator/src 10 | namespace LLM\Agents\PromptGenerator; 11 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 12 | interface PromptInterceptorInterface 13 | { 14 | public function generate( 15 | PromptGeneratorInput $input, 16 | InterceptorHandler $next, 17 | ): PromptInterface; 18 | } 19 | //vendor/llm-agents/prompt-generator/src/Integration/Laravel 20 | namespace LLM\Agents\PromptGenerator\Integration\Laravel; 21 | use Illuminate\Support\ServiceProvider; 22 | use LLM\Agents\LLM\AgentPromptGeneratorInterface; 23 | use LLM\Agents\PromptGenerator\PromptGeneratorPipeline; 24 | use LLM\Agents\PromptGenerator\PromptGeneratorPipelineInterface; 25 | final class PromptGeneratorServiceProvider extends ServiceProvider 26 | { 27 | public function register(): void 28 | { 29 | $this->app->singleton(PromptGeneratorPipeline::class, PromptGeneratorPipeline::class); 30 | $this->app->singleton(PromptGeneratorPipelineInterface::class, PromptGeneratorPipeline::class); 31 | $this->app->singleton(AgentPromptGeneratorInterface::class, PromptGeneratorPipeline::class); 32 | } 33 | } 34 | //vendor/llm-agents/prompt-generator/src/Integration/Spiral 35 | namespace LLM\Agents\PromptGenerator\Integration\Spiral; 36 | use LLM\Agents\LLM\AgentPromptGeneratorInterface; 37 | use LLM\Agents\PromptGenerator\PromptGeneratorPipeline; 38 | use LLM\Agents\PromptGenerator\PromptGeneratorPipelineInterface; 39 | use Spiral\Boot\Bootloader\Bootloader; 40 | final class PromptGeneratorBootloader extends Bootloader 41 | { 42 | public function defineSingletons(): array 43 | { 44 | return [ 45 | PromptGeneratorPipeline::class => PromptGeneratorPipeline::class, 46 | PromptGeneratorPipelineInterface::class => PromptGeneratorPipeline::class, 47 | AgentPromptGeneratorInterface::class => PromptGeneratorPipeline::class, 48 | ]; 49 | } 50 | } 51 | //vendor/llm-agents/prompt-generator/src 52 | namespace LLM\Agents\PromptGenerator; 53 | use LLM\Agents\LLM\AgentPromptGeneratorInterface; 54 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 55 | final readonly class InterceptorHandler 56 | { 57 | public function __construct( 58 | private AgentPromptGeneratorInterface $generator, 59 | ) {} 60 | public function __invoke(PromptGeneratorInput $input): PromptInterface 61 | { 62 | return $this->generator->generate( 63 | agent: $input->agent, 64 | userPrompt: $input->userPrompt, 65 | context: $input->context, 66 | prompt: $input->prompt, 67 | ); 68 | } 69 | } 70 | //vendor/llm-agents/prompt-generator/src 71 | namespace LLM\Agents\PromptGenerator; 72 | use LLM\Agents\Agent\AgentInterface; 73 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 74 | use LLM\Agents\LLM\PromptContextInterface; 75 | final readonly class PromptGeneratorInput 76 | { 77 | public function __construct( 78 | public AgentInterface $agent, 79 | public string|\Stringable $userPrompt, 80 | public PromptInterface $prompt, 81 | public PromptContextInterface $context, 82 | ) {} 83 | public function withAgent(AgentInterface $agent): self 84 | { 85 | return new self( 86 | $agent, 87 | $this->userPrompt, 88 | $this->prompt, 89 | $this->context, 90 | ); 91 | } 92 | public function withUserPrompt(string|\Stringable $userPrompt): self 93 | { 94 | return new self( 95 | $this->agent, 96 | $userPrompt, 97 | $this->prompt, 98 | $this->context, 99 | ); 100 | } 101 | public function withContext(PromptContextInterface $context): self 102 | { 103 | return new self( 104 | $this->agent, 105 | $this->userPrompt, 106 | $this->prompt, 107 | $context, 108 | ); 109 | } 110 | public function withPrompt(PromptInterface $prompt): self 111 | { 112 | return new self( 113 | $this->agent, 114 | $this->userPrompt, 115 | $prompt, 116 | $this->context, 117 | ); 118 | } 119 | } 120 | //vendor/llm-agents/prompt-generator/src 121 | namespace LLM\Agents\PromptGenerator; 122 | use LLM\Agents\Agent\AgentInterface; 123 | use LLM\Agents\LLM\Prompt\Chat\Prompt; 124 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 125 | use LLM\Agents\LLM\PromptContextInterface; 126 | final class PromptGeneratorPipeline implements PromptGeneratorPipelineInterface 127 | { 128 | /** @var PromptInterceptorInterface[] */ 129 | private array $interceptors = []; 130 | private int $offset = 0; 131 | public function generate( 132 | AgentInterface $agent, 133 | string|\Stringable $userPrompt, 134 | PromptContextInterface $context, 135 | PromptInterface $prompt = new Prompt(), 136 | ): PromptInterface { 137 | if (!isset($this->interceptors[$this->offset])) { 138 | return $prompt; 139 | } 140 | return $this->interceptors[$this->offset]->generate( 141 | input: new PromptGeneratorInput( 142 | agent: $agent, 143 | userPrompt: $userPrompt, 144 | prompt: $prompt, 145 | context: $context, 146 | ), 147 | next: new InterceptorHandler(generator: $this->next()), 148 | ); 149 | } 150 | public function withInterceptor(PromptInterceptorInterface ...$interceptor): self 151 | { 152 | $pipeline = clone $this; 153 | $pipeline->interceptors = \array_merge($this->interceptors, $interceptor); 154 | return $pipeline; 155 | } 156 | private function next(): self 157 | { 158 | $pipeline = clone $this; 159 | $pipeline->offset++; 160 | return $pipeline; 161 | } 162 | } 163 | //vendor/llm-agents/prompt-generator/src/Interceptors 164 | namespace LLM\Agents\PromptGenerator\Interceptors; 165 | use LLM\Agents\LLM\Prompt\Chat\MessagePrompt; 166 | use LLM\Agents\LLM\Prompt\Chat\Prompt; 167 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 168 | use LLM\Agents\PromptGenerator\InterceptorHandler; 169 | use LLM\Agents\PromptGenerator\PromptGeneratorInput; 170 | use LLM\Agents\PromptGenerator\PromptInterceptorInterface; 171 | use LLM\Agents\Solution\SolutionMetadata; 172 | final class AgentMemoryInjector implements PromptInterceptorInterface 173 | { 174 | public function generate( 175 | PromptGeneratorInput $input, 176 | InterceptorHandler $next, 177 | ): PromptInterface { 178 | \assert($input->prompt instanceof Prompt); 179 | return $next( 180 | input: $input->withPrompt( 181 | $input->prompt 182 | ->withAddedMessage( 183 | MessagePrompt::system( 184 | prompt: 'Instructions about your experiences, follow them: {memory}. And also {dynamic_memory}', 185 | ), 186 | ) 187 | ->withValues( 188 | values: [ 189 | 'memory' => \implode( 190 | PHP_EOL, 191 | \array_map( 192 | static fn(SolutionMetadata $metadata) => $metadata->content, 193 | $input->agent->getMemory(), 194 | ), 195 | ), 196 | 'dynamic_memory' => '', 197 | ], 198 | ), 199 | ), 200 | ); 201 | } 202 | } 203 | //vendor/llm-agents/prompt-generator/src/Interceptors 204 | namespace LLM\Agents\PromptGenerator\Interceptors; 205 | use LLM\Agents\LLM\Prompt\Chat\ChatMessage; 206 | use LLM\Agents\LLM\Prompt\Chat\Prompt; 207 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 208 | use LLM\Agents\LLM\Prompt\Chat\Role; 209 | use LLM\Agents\PromptGenerator\InterceptorHandler; 210 | use LLM\Agents\PromptGenerator\PromptGeneratorInput; 211 | use LLM\Agents\PromptGenerator\PromptInterceptorInterface; 212 | final class UserPromptInjector implements PromptInterceptorInterface 213 | { 214 | public function generate( 215 | PromptGeneratorInput $input, 216 | InterceptorHandler $next, 217 | ): PromptInterface { 218 | \assert($input->prompt instanceof Prompt); 219 | return $next( 220 | input: $input->withPrompt( 221 | $input->prompt->withAddedMessage( 222 | new ChatMessage( 223 | content: (string) $input->userPrompt, 224 | role: Role::User, 225 | ), 226 | ), 227 | ), 228 | ); 229 | } 230 | } 231 | //vendor/llm-agents/prompt-generator/src/Interceptors 232 | namespace LLM\Agents\PromptGenerator\Interceptors; 233 | use LLM\Agents\Agent\AgentRepositoryInterface; 234 | use LLM\Agents\Agent\HasLinkedAgentsInterface; 235 | use LLM\Agents\LLM\Prompt\Chat\MessagePrompt; 236 | use LLM\Agents\LLM\Prompt\Chat\Prompt; 237 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 238 | use LLM\Agents\PromptGenerator\InterceptorHandler; 239 | use LLM\Agents\PromptGenerator\PromptGeneratorInput; 240 | use LLM\Agents\PromptGenerator\PromptInterceptorInterface; 241 | use LLM\Agents\Solution\AgentLink; 242 | use LLM\Agents\Tool\SchemaMapperInterface; 243 | final readonly class LinkedAgentsInjector implements PromptInterceptorInterface 244 | { 245 | public function __construct( 246 | private AgentRepositoryInterface $agents, 247 | private SchemaMapperInterface $schemaMapper, 248 | ) {} 249 | public function generate( 250 | PromptGeneratorInput $input, 251 | InterceptorHandler $next, 252 | ): PromptInterface { 253 | \assert($input->prompt instanceof Prompt); 254 | if (!$input->agent instanceof HasLinkedAgentsInterface) { 255 | return $next($input); 256 | } 257 | if (\count($input->agent->getAgents()) === 0) { 258 | return $next($input); 259 | } 260 | $associatedAgents = \array_map( 261 | fn(AgentLink $agent): array => [ 262 | 'agent' => $this->agents->get($agent->getName()), 263 | 'output_schema' => \json_encode($this->schemaMapper->toJsonSchema($agent->outputSchema)), 264 | ], 265 | $input->agent->getAgents(), 266 | ); 267 | return $next( 268 | input: $input->withPrompt( 269 | $input 270 | ->prompt 271 | ->withAddedMessage( 272 | MessagePrompt::system( 273 | prompt: <<<'PROMPT' 274 | There are agents {linked_agents} associated with you. You can ask them for help if you need it. 275 | Use the `ask_agent` tool and provide the agent key. 276 | Always follow rules: 277 | - Don't make up the agent key. Use only the ones from the provided list. 278 | PROMPT, 279 | ), 280 | ) 281 | ->withValues( 282 | values: [ 283 | 'linked_agents' => \implode( 284 | PHP_EOL, 285 | \array_map( 286 | static fn(array $agent): string => \json_encode([ 287 | 'key' => $agent['agent']->getKey(), 288 | 'description' => $agent['agent']->getDescription(), 289 | 'output_schema' => $agent['output_schema'], 290 | ]), 291 | $associatedAgents, 292 | ), 293 | ), 294 | ], 295 | ), 296 | ), 297 | ); 298 | } 299 | } 300 | //vendor/llm-agents/prompt-generator/src/Interceptors 301 | namespace LLM\Agents\PromptGenerator\Interceptors; 302 | use LLM\Agents\LLM\Prompt\Chat\MessagePrompt; 303 | use LLM\Agents\LLM\Prompt\Chat\Prompt; 304 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface; 305 | use LLM\Agents\PromptGenerator\InterceptorHandler; 306 | use LLM\Agents\PromptGenerator\PromptGeneratorInput; 307 | use LLM\Agents\PromptGenerator\PromptInterceptorInterface; 308 | final class InstructionGenerator implements PromptInterceptorInterface 309 | { 310 | public function generate( 311 | PromptGeneratorInput $input, 312 | InterceptorHandler $next, 313 | ): PromptInterface { 314 | \assert($input->prompt instanceof Prompt); 315 | return $next( 316 | input: $input->withPrompt( 317 | $input->prompt 318 | ->withAddedMessage( 319 | MessagePrompt::system( 320 | prompt: <<<'PROMPT' 321 | {prompt} 322 | Important rules: 323 | - always response in markdown format 324 | - think before responding to user 325 | PROMPT, 326 | ), 327 | ) 328 | ->withValues( 329 | values: [ 330 | 'prompt' => $input->agent->getInstruction(), 331 | ], 332 | ), 333 | ), 334 | ); 335 | } 336 | } 337 | //vendor/llm-agents/prompt-generator/src 338 | namespace LLM\Agents\PromptGenerator; 339 | use LLM\Agents\LLM\PromptContextInterface; 340 | class Context implements PromptContextInterface 341 | { 342 | private array|\JsonSerializable|null $authContext = null; 343 | final public static function new(): self 344 | { 345 | return new self(); 346 | } 347 | public function setAuthContext(array|\JsonSerializable $authContext): self 348 | { 349 | $this->authContext = $authContext; 350 | return $this; 351 | } 352 | public function getAuthContext(): array|\JsonSerializable|null 353 | { 354 | return $this->authContext; 355 | } 356 | } 357 | //vendor/llm-agents/openai-client/src 358 | namespace LLM\Agents\OpenAI\Client; 359 | use LLM\Agents\LLM\Prompt\Chat\ChatMessage; 360 | use LLM\Agents\LLM\Prompt\Chat\Role; 361 | use LLM\Agents\LLM\Prompt\Chat\ToolCalledPrompt; 362 | use LLM\Agents\LLM\Prompt\Chat\ToolCallResultMessage; 363 | use LLM\Agents\LLM\Prompt\Tool; 364 | use LLM\Agents\LLM\Response\ToolCall; 365 | final readonly class MessageMapper 366 | { 367 | public function map(object $message): array 368 | { 369 | if ($message instanceof ChatMessage) { 370 | return [ 371 | 'content' => $message->content, 372 | 'role' => $message->role->value, 373 | ]; 374 | } 375 | if ($message instanceof ToolCallResultMessage) { 376 | return [ 377 | 'content' => \is_array($message->content) ? \json_encode($message->content) : $message->content, 378 | 'tool_call_id' => $message->id, 379 | 'role' => $message->role->value, 380 | ]; 381 | } 382 | if ($message instanceof ToolCalledPrompt) { 383 | return [ 384 | 'content' => null, 385 | 'role' => Role::Assistant->value, 386 | 'tool_calls' => \array_map( 387 | static fn(ToolCall $tool): array => [ 388 | 'id' => $tool->id, 389 | 'type' => 'function', 390 | 'function' => [ 391 | 'name' => $tool->name, 392 | 'arguments' => $tool->arguments, 393 | ], 394 | ], 395 | $message->tools, 396 | ), 397 | ]; 398 | } 399 | if ($message instanceof Tool) { 400 | return [ 401 | 'type' => 'function', 402 | 'function' => [ 403 | 'name' => $message->name, 404 | 'description' => $message->description, 405 | 'parameters' => [ 406 | 'type' => 'object', 407 | 'additionalProperties' => $message->additionalProperties, 408 | ] + $message->parameters, 409 | 'strict' => $message->strict, 410 | ], 411 | ]; 412 | } 413 | if ($message instanceof \JsonSerializable) { 414 | return $message->jsonSerialize(); 415 | } 416 | throw new \InvalidArgumentException('Invalid message type'); 417 | } 418 | } 419 | //vendor/llm-agents/openai-client/src/Integration/Laravel 420 | namespace LLM\Agents\OpenAI\Client\Integration\Laravel; 421 | use Illuminate\Contracts\Foundation\Application; 422 | use Illuminate\Support\ServiceProvider; 423 | use LLM\Agents\Embeddings\EmbeddingGeneratorInterface; 424 | use LLM\Agents\LLM\LLMInterface; 425 | use LLM\Agents\OpenAI\Client\Embeddings\EmbeddingGenerator; 426 | use LLM\Agents\OpenAI\Client\Embeddings\OpenAIEmbeddingModel; 427 | use LLM\Agents\OpenAI\Client\LLM; 428 | use LLM\Agents\OpenAI\Client\Parsers\ChatResponseParser; 429 | use LLM\Agents\OpenAI\Client\StreamResponseParser; 430 | use OpenAI\Contracts\ClientContract; 431 | use OpenAI\Responses\Chat\CreateStreamedResponse; 432 | final class OpenAIClientServiceProvider extends ServiceProvider 433 | { 434 | public function register(): void 435 | { 436 | $this->app->singleton( 437 | LLMInterface::class, 438 | LLM::class, 439 | ); 440 | $this->app->singleton( 441 | EmbeddingGeneratorInterface::class, 442 | EmbeddingGenerator::class, 443 | ); 444 | $this->app->singleton( 445 | EmbeddingGenerator::class, 446 | static function ( 447 | ClientContract $client, 448 | ): EmbeddingGenerator { 449 | return new EmbeddingGenerator( 450 | client: $client, 451 | // todo: use config 452 | model: OpenAIEmbeddingModel::TextEmbeddingAda002, 453 | ); 454 | }, 455 | ); 456 | $this->app->singleton( 457 | StreamResponseParser::class, 458 | static function (Application $app): StreamResponseParser { 459 | $parser = new StreamResponseParser(); 460 | // Register parsers here 461 | $parser->registerParser( 462 | CreateStreamedResponse::class, 463 | $app->make(ChatResponseParser::class), 464 | ); 465 | return $parser; 466 | }, 467 | ); 468 | } 469 | } 470 | //vendor/llm-agents/openai-client/src/Integration/Spiral 471 | namespace LLM\Agents\OpenAI\Client\Integration\Spiral; 472 | use GuzzleHttp\Client as HttpClient; 473 | use LLM\Agents\Embeddings\EmbeddingGeneratorInterface; 474 | use LLM\Agents\LLM\LLMInterface; 475 | use LLM\Agents\OpenAI\Client\Embeddings\EmbeddingGenerator; 476 | use LLM\Agents\OpenAI\Client\Embeddings\OpenAIEmbeddingModel; 477 | use LLM\Agents\OpenAI\Client\LLM; 478 | use LLM\Agents\OpenAI\Client\Parsers\ChatResponseParser; 479 | use LLM\Agents\OpenAI\Client\StreamResponseParser; 480 | use OpenAI\Contracts\ClientContract; 481 | use OpenAI\Responses\Chat\CreateStreamedResponse; 482 | use Spiral\Boot\Bootloader\Bootloader; 483 | use Spiral\Boot\EnvironmentInterface; 484 | final class OpenAIClientBootloader extends Bootloader 485 | { 486 | public function defineSingletons(): array 487 | { 488 | return [ 489 | LLMInterface::class => LLM::class, 490 | EmbeddingGeneratorInterface::class => EmbeddingGenerator::class, 491 | EmbeddingGenerator::class => static function ( 492 | ClientContract $client, 493 | EnvironmentInterface $env, 494 | ): EmbeddingGenerator { 495 | return new EmbeddingGenerator( 496 | client: $client, 497 | model: OpenAIEmbeddingModel::from( 498 | $env->get('OPENAI_EMBEDDING_MODEL', OpenAIEmbeddingModel::TextEmbedding3Small->value), 499 | ), 500 | ); 501 | }, 502 | ClientContract::class => static fn( 503 | EnvironmentInterface $env, 504 | ): ClientContract => \OpenAI::factory() 505 | ->withApiKey($env->get('OPENAI_KEY')) 506 | ->withHttpHeader('OpenAI-Beta', 'assistants=v1') 507 | ->withHttpClient( 508 | new HttpClient([ 509 | 'timeout' => (int) $env->get('OPENAI_HTTP_CLIENT_TIMEOUT', 2 * 60), 510 | ]), 511 | ) 512 | ->make(), 513 | StreamResponseParser::class => static function ( 514 | ChatResponseParser $chatResponseParser, 515 | ): StreamResponseParser { 516 | $parser = new StreamResponseParser(); 517 | // Register parsers here 518 | $parser->registerParser(CreateStreamedResponse::class, $chatResponseParser); 519 | return $parser; 520 | }, 521 | ]; 522 | } 523 | } 524 | //vendor/llm-agents/openai-client/src/Embeddings 525 | namespace LLM\Agents\OpenAI\Client\Embeddings; 526 | use LLM\Agents\Embeddings\Document; 527 | use LLM\Agents\Embeddings\Embedding; 528 | use LLM\Agents\Embeddings\EmbeddingGeneratorInterface; 529 | use OpenAI\Contracts\ClientContract; 530 | final readonly class EmbeddingGenerator implements EmbeddingGeneratorInterface 531 | { 532 | public function __construct( 533 | private ClientContract $client, 534 | private OpenAIEmbeddingModel $model = OpenAIEmbeddingModel::TextEmbeddingAda002, 535 | ) {} 536 | public function generate(Document ...$documents): array 537 | { 538 | $documents = \array_values($documents); 539 | $response = $this->client->embeddings()->create([ 540 | 'model' => $this->model->value, 541 | 'input' => \array_map(static fn(Document $doc): string => $doc->content, $documents), 542 | ]); 543 | foreach ($response->embeddings as $i => $embedding) { 544 | $documents[$i] = $documents[$i]->withEmbedding(new Embedding($embedding->embedding)); 545 | } 546 | return $documents; 547 | } 548 | } 549 | //vendor/llm-agents/openai-client/src/Embeddings 550 | namespace LLM\Agents\OpenAI\Client\Embeddings; 551 | enum OpenAIEmbeddingModel: string 552 | { 553 | case TextEmbedding3Small = 'text-embedding-3-small'; 554 | case TextEmbedding3Large = 'text-embedding-3-large'; 555 | case TextEmbeddingAda002 = 'text-embedding-ada-002'; 556 | } 557 | //vendor/llm-agents/openai-client/src 558 | namespace LLM\Agents\OpenAI\Client; 559 | use LLM\Agents\LLM\ContextFactoryInterface; 560 | use LLM\Agents\LLM\ContextInterface; 561 | final class ContextFactory implements ContextFactoryInterface 562 | { 563 | public function create(): ContextInterface 564 | { 565 | return new Context(); 566 | } 567 | } 568 | //vendor/llm-agents/openai-client/src 569 | namespace LLM\Agents\OpenAI\Client; 570 | use LLM\Agents\LLM\OptionsFactoryInterface; 571 | use LLM\Agents\LLM\OptionsInterface; 572 | final class OptionsFactory implements OptionsFactoryInterface 573 | { 574 | public function create(array $options = []): OptionsInterface 575 | { 576 | return new Options($options); 577 | } 578 | } 579 | //vendor/llm-agents/openai-client/src 580 | namespace LLM\Agents\OpenAI\Client; 581 | enum OpenAIModel: string 582 | { 583 | case Gpt4o = 'gpt-4o'; 584 | case Gpt4oLatest = 'chatgpt-4o-latest'; 585 | case Gpt4o20240513 = 'gpt-4o-2024-05-13'; 586 | case Gpt4o20240806 = 'gpt-4o-2024-08-06'; 587 | case Gpt4oMini = 'gpt-4o-mini'; 588 | case Gpt4oMini20240718 = 'gpt-4o-mini-2024-07-18'; 589 | case Gpt4Turbo = 'gpt-4-turbo'; 590 | case Gpt4TurboPreview = 'gpt-4-turbo-preview'; 591 | case Gpt4 = 'gpt-4'; 592 | case Gpt40613 = 'gpt-4-0613'; 593 | case Gpt3Turbo = 'gpt-3.5-turbo'; 594 | case Gpt3Turbo1106 = 'gpt-3.5-turbo-1106'; 595 | case Gpt3TurboInstruct = 'gpt-3.5-turbo-instruct'; 596 | } 597 | //vendor/llm-agents/openai-client/src 598 | namespace LLM\Agents\OpenAI\Client; 599 | use LLM\Agents\LLM\ContextInterface; 600 | use LLM\Agents\LLM\LLMInterface; 601 | use LLM\Agents\LLM\OptionsInterface; 602 | use LLM\Agents\LLM\Prompt\Chat\MessagePrompt; 603 | use LLM\Agents\LLM\Prompt\Chat\PromptInterface as ChatPromptInterface; 604 | use LLM\Agents\LLM\Prompt\PromptInterface; 605 | use LLM\Agents\LLM\Prompt\Tool; 606 | use LLM\Agents\LLM\Response\Response; 607 | use LLM\Agents\OpenAI\Client\Exception\LimitExceededException; 608 | use LLM\Agents\OpenAI\Client\Exception\RateLimitException; 609 | use LLM\Agents\OpenAI\Client\Exception\TimeoutException; 610 | use OpenAI\Contracts\ClientContract; 611 | final class LLM implements LLMInterface 612 | { 613 | private array $defaultOptions = [ 614 | Option::Temperature->value => 0.8, 615 | Option::MaxTokens->value => 120, 616 | Option::TopP->value => null, 617 | Option::FrequencyPenalty->value => null, 618 | Option::PresencePenalty->value => null, 619 | Option::Stop->value => null, 620 | Option::LogitBias->value => null, 621 | Option::FunctionCall->value => null, 622 | Option::Functions->value => null, 623 | Option::User->value => null, 624 | Option::Model->value => OpenAIModel::Gpt4oMini->value, 625 | ]; 626 | public function __construct( 627 | private readonly ClientContract $client, 628 | private readonly MessageMapper $messageMapper, 629 | protected readonly StreamResponseParser $streamParser, 630 | ) {} 631 | public function generate( 632 | ContextInterface $context, 633 | PromptInterface $prompt, 634 | OptionsInterface $options, 635 | ): Response { 636 | \assert($options instanceof Options); 637 | $request = $this->buildOptions($options); 638 | if ($prompt instanceof ChatPromptInterface) { 639 | $messages = $prompt->format(); 640 | } else { 641 | $messages = [ 642 | MessagePrompt::user($prompt)->toChatMessage(), 643 | ]; 644 | } 645 | foreach ($messages as $message) { 646 | $request['messages'][] = $this->messageMapper->map($message); 647 | } 648 | if ($options->has(Option::Tools)) { 649 | $request['tools'] = \array_values( 650 | \array_map( 651 | fn(Tool $tool): array => $this->messageMapper->map($tool), 652 | $options->get(Option::Tools), 653 | ), 654 | ); 655 | } 656 | $callback = null; 657 | if ($options->has(Option::StreamChunkCallback)) { 658 | $callback = $options->get(Option::StreamChunkCallback); 659 | \assert($callback instanceof StreamChunkCallbackInterface); 660 | } 661 | $stream = $this 662 | ->client 663 | ->chat() 664 | ->createStreamed($request); 665 | try { 666 | return $this->streamParser->parse($stream, $callback); 667 | } catch (LimitExceededException) { 668 | throw new \LLM\Agents\LLM\Exception\LimitExceededException( 669 | currentLimit: $request['max_tokens'], 670 | ); 671 | } catch (RateLimitException) { 672 | throw new \LLM\Agents\LLM\Exception\RateLimitException(); 673 | } catch (TimeoutException) { 674 | throw new \LLM\Agents\LLM\Exception\TimeoutException(); 675 | } 676 | } 677 | protected function buildOptions(OptionsInterface $options): array 678 | { 679 | $result = $this->defaultOptions; 680 | // only keys that present in default options should be replaced 681 | foreach ($options as $key => $value) { 682 | if (isset($this->defaultOptions[$key])) { 683 | $result[$key] = $value; 684 | } 685 | } 686 | if (!isset($result['model'])) { 687 | throw new \InvalidArgumentException('Model is required'); 688 | } 689 | // filter out null options 690 | return \array_filter($result, static fn($value): bool => $value !== null); 691 | } 692 | } 693 | //vendor/llm-agents/openai-client/src 694 | namespace LLM\Agents\OpenAI\Client; 695 | interface StreamChunkCallbackInterface 696 | { 697 | public function __invoke(?string $chunk, bool $stop, ?string $finishReason = null): void; 698 | } 699 | //vendor/llm-agents/openai-client/src/Event 700 | namespace LLM\Agents\OpenAI\Client\Event; 701 | final readonly class ToolCall 702 | { 703 | public function __construct( 704 | public string $id, 705 | public string $name, 706 | public string $arguments, 707 | ) {} 708 | } 709 | //vendor/llm-agents/openai-client/src/Event 710 | namespace LLM\Agents\OpenAI\Client\Event; 711 | final readonly class MessageChunk 712 | { 713 | public function __construct( 714 | public string $chunk, 715 | public bool $stop, 716 | public ?string $finishReason = null, 717 | ) {} 718 | } 719 | //vendor/llm-agents/openai-client/src 720 | namespace LLM\Agents\OpenAI\Client; 721 | use LLM\Agents\LLM\ContextInterface; 722 | final class Context implements ContextInterface 723 | { 724 | } 725 | //vendor/llm-agents/openai-client/src 726 | namespace LLM\Agents\OpenAI\Client; 727 | use LLM\Agents\LLM\OptionsInterface; 728 | use Traversable; 729 | readonly class Options implements OptionsInterface 730 | { 731 | public function __construct( 732 | private array $options = [], 733 | ) {} 734 | public function withModel(OpenAIModel $model): static 735 | { 736 | return $this->with(Option::Model, $model->value); 737 | } 738 | public function withTemperature(float $temperature): static 739 | { 740 | return $this->with(Option::Temperature, $temperature); 741 | } 742 | public function withMaxTokens(int $maxTokens): static 743 | { 744 | return $this->with(Option::MaxTokens, $maxTokens); 745 | } 746 | public function getIterator(): Traversable 747 | { 748 | return new \ArrayIterator($this->options); 749 | } 750 | public function has(string|Option $option): bool 751 | { 752 | return isset($this->options[$this->prepareKey($option)]); 753 | } 754 | public function get(string|Option $option, mixed $default = null): mixed 755 | { 756 | return $this->options[$this->prepareKey($option)] ?? $default; 757 | } 758 | public function with(string|Option $option, mixed $value): static 759 | { 760 | $options = $this->options; 761 | $options[$this->prepareKey($option)] = $value; 762 | return new static($options); 763 | } 764 | private function prepareKey(string|Option $key): string 765 | { 766 | return match (true) { 767 | $key instanceof Option => $key->value, 768 | default => $key, 769 | }; 770 | } 771 | public function merge(OptionsInterface $options): static 772 | { 773 | $mergedOptions = $this->options; 774 | foreach ($options as $key => $value) { 775 | $mergedOptions[$key] = $value; 776 | } 777 | return new static($mergedOptions); 778 | } 779 | } 780 | //vendor/llm-agents/openai-client/src/Exception 781 | namespace LLM\Agents\OpenAI\Client\Exception; 782 | final class RateLimitException extends OpenAiClientException 783 | { 784 | } 785 | //vendor/llm-agents/openai-client/src/Exception 786 | namespace LLM\Agents\OpenAI\Client\Exception; 787 | final class TimeoutException extends OpenAiClientException 788 | { 789 | } 790 | //vendor/llm-agents/openai-client/src/Exception 791 | namespace LLM\Agents\OpenAI\Client\Exception; 792 | class OpenAiClientException extends \Exception 793 | { 794 | } 795 | //vendor/llm-agents/openai-client/src/Exception 796 | namespace LLM\Agents\OpenAI\Client\Exception; 797 | final class LimitExceededException extends OpenAiClientException 798 | { 799 | } 800 | //vendor/llm-agents/openai-client/src 801 | namespace LLM\Agents\OpenAI\Client; 802 | enum Option: string 803 | { 804 | // Configuration options 805 | case Temperature = 'temperature'; 806 | case MaxTokens = 'max_tokens'; 807 | case TopP = 'top_p'; 808 | case FrequencyPenalty = 'frequency_penalty'; 809 | case PresencePenalty = 'presence_penalty'; 810 | case Stop = 'stop'; 811 | case LogitBias = 'logit_bias'; 812 | case Functions = 'functions'; 813 | case FunctionCall = 'function_call'; 814 | case User = 'user'; 815 | case Model = 'model'; 816 | // Application options 817 | case Tools = 'tools'; 818 | case StreamChunkCallback = 'stream_chunk_callback'; 819 | } 820 | //vendor/llm-agents/openai-client/src 821 | namespace LLM\Agents\OpenAI\Client; 822 | use LLM\Agents\OpenAI\Client\Exception\LimitExceededException; 823 | use LLM\Agents\OpenAI\Client\Exception\RateLimitException; 824 | use LLM\Agents\OpenAI\Client\Exception\TimeoutException; 825 | use LLM\Agents\OpenAI\Client\Parsers\ParserInterface; 826 | use LLM\Agents\LLM\Exception\LLMException; 827 | use LLM\Agents\LLM\Response\Response; 828 | use OpenAI\Responses\StreamResponse; 829 | use Psr\Http\Message\ResponseInterface; 830 | final class StreamResponseParser 831 | { 832 | private array $parsers = []; 833 | public function registerParser(string $type, ParserInterface $parser): void 834 | { 835 | $this->parsers[$type] = $parser; 836 | } 837 | /** 838 | * @throws LimitExceededException 839 | * @throws RateLimitException 840 | * @throws TimeoutException 841 | */ 842 | public function parse(StreamResponse $stream, ?StreamChunkCallbackInterface $callback = null): Response 843 | { 844 | $this->validateStreamResponse($stream); 845 | $headers = $this->getHeaders($stream); 846 | $responseClass = $this->getResponseClass($stream); 847 | foreach ($this->parsers as $type => $parser) { 848 | if ($responseClass === $type) { 849 | return $parser->parse($stream, $callback); 850 | } 851 | } 852 | throw new LLMException( 853 | \sprintf( 854 | 'Parser not found for response class: %s', 855 | $responseClass, 856 | ), 857 | ); 858 | } 859 | private function getHeaders(StreamResponse $stream): array 860 | { 861 | $headers = []; 862 | $response = $this->fetchResponse($stream); 863 | foreach ($response->getHeaders() as $name => $values) { 864 | $value = $response->getHeaderLine($name); 865 | // mapping value type 866 | if (\is_numeric($value)) { 867 | $value = (float) $value; 868 | } 869 | $headers[$name] = $value; 870 | } 871 | return $headers; 872 | } 873 | private function validateStreamResponse(StreamResponse $stream): void 874 | { 875 | $response = $this->fetchResponse($stream); 876 | if ($response->getStatusCode() !== 200) { 877 | try { 878 | $error = \json_decode($response->getBody()->getContents()); 879 | } catch (\Throwable) { 880 | throw new LLMException( 881 | \sprintf( 882 | 'OpenAI API returned status code %s', 883 | $response->getStatusCode(), 884 | ), 885 | $response->getStatusCode(), 886 | ); 887 | } 888 | $message = $error->error->message; 889 | if ($message === '') { 890 | $message = $error->error->code; 891 | } 892 | throw new LLMException($message); 893 | } 894 | } 895 | private function fetchResponse(StreamResponse $response): ResponseInterface 896 | { 897 | $closure = \Closure::bind(function (StreamResponse $class) { 898 | return $class->response; 899 | }, null, StreamResponse::class); 900 | return $closure($response); 901 | } 902 | public function getResponseClass(StreamResponse $stream): mixed 903 | { 904 | // todo: find a better way to get response class 905 | $reflection = new \ReflectionClass($stream); 906 | $responseClass = $reflection->getProperty('responseClass'); 907 | $responseClass->setAccessible(true); 908 | $responseClass = $responseClass->getValue($stream); 909 | return $responseClass; 910 | } 911 | } 912 | //vendor/llm-agents/openai-client/src/Parsers 913 | namespace LLM\Agents\OpenAI\Client\Parsers; 914 | use LLM\Agents\OpenAI\Client\Event\MessageChunk; 915 | use LLM\Agents\OpenAI\Client\Exception\LimitExceededException; 916 | use LLM\Agents\OpenAI\Client\Exception\RateLimitException; 917 | use LLM\Agents\OpenAI\Client\Exception\TimeoutException; 918 | use LLM\Agents\OpenAI\Client\StreamChunkCallbackInterface; 919 | use LLM\Agents\LLM\Response\FinishReason; 920 | use LLM\Agents\LLM\Response\Response; 921 | use LLM\Agents\LLM\Response\StreamChatResponse; 922 | use LLM\Agents\LLM\Response\ToolCall; 923 | use LLM\Agents\LLM\Response\ToolCalledResponse; 924 | use OpenAI\Contracts\ResponseStreamContract; 925 | use OpenAI\Responses\Chat\CreateStreamedResponse; 926 | use Psr\EventDispatcher\EventDispatcherInterface; 927 | final readonly class ChatResponseParser implements ParserInterface 928 | { 929 | public function __construct( 930 | private ?EventDispatcherInterface $eventDispatcher = null, 931 | ) {} 932 | /** 933 | * @throws LimitExceededException 934 | * @throws RateLimitException 935 | * @throws TimeoutException 936 | */ 937 | public function parse(ResponseStreamContract $stream, ?StreamChunkCallbackInterface $callback = null): Response 938 | { 939 | $callback ??= static fn(?string $chunk, bool $stop, ?string $finishReason = null) => null; 940 | $result = ''; 941 | $finishReason = null; 942 | /** @var ToolCall[] $toolCalls */ 943 | $toolCalls = []; 944 | $toolIndex = 0; 945 | /** @var CreateStreamedResponse[] $stream */ 946 | foreach ($stream as $chunk) { 947 | if ($chunk->choices[0]->finishReason !== null) { 948 | $callback(chunk: '', stop: true, finishReason: $chunk->choices[0]->finishReason); 949 | $this->eventDispatcher?->dispatch( 950 | new MessageChunk( 951 | chunk: '', 952 | stop: true, 953 | finishReason: $chunk->choices[0]->finishReason, 954 | ), 955 | ); 956 | $finishReason = FinishReason::from($chunk->choices[0]->finishReason); 957 | break; 958 | } 959 | if ($chunk->choices[0]->delta->role !== null) { 960 | // For single tool call 961 | if ($chunk->choices[0]->delta?->toolCalls !== []) { 962 | foreach ($chunk->choices[0]->delta->toolCalls as $i => $toolCall) { 963 | $toolCalls[$toolIndex] = new ToolCall( 964 | id: $toolCall->id, 965 | name: $toolCall->function->name, 966 | arguments: '', 967 | ); 968 | } 969 | } 970 | continue; 971 | } 972 | if ($chunk->choices[0]->delta?->toolCalls !== []) { 973 | foreach ($chunk->choices[0]->delta->toolCalls as $i => $toolCall) { 974 | // For multiple tool calls 975 | if ($toolCall->id !== null) { 976 | $toolIndex++; 977 | $toolCalls[$toolIndex] = new ToolCall( 978 | id: $toolCall->id, 979 | name: $toolCall->function->name, 980 | arguments: '', 981 | ); 982 | continue; 983 | } 984 | $toolCalls[$toolIndex] = $toolCalls[$toolIndex]->withArguments($toolCall->function->arguments); 985 | } 986 | continue; 987 | } 988 | $callback(chunk: $chunk->choices[0]->delta->content, stop: false); 989 | $this->eventDispatcher->dispatch( 990 | new MessageChunk( 991 | chunk: $chunk->choices[0]->delta->content, 992 | stop: false, 993 | finishReason: $chunk->choices[0]->finishReason, 994 | ), 995 | ); 996 | $result .= $chunk->choices[0]->delta->content; 997 | } 998 | foreach ($toolCalls as $toolCall) { 999 | $this->eventDispatcher?->dispatch( 1000 | new \LLM\Agents\OpenAI\Client\Event\ToolCall( 1001 | id: $toolCall->id, 1002 | name: $toolCall->name, 1003 | arguments: $toolCall->arguments, 1004 | ), 1005 | ); 1006 | } 1007 | return match (true) { 1008 | $finishReason === FinishReason::Stop => new StreamChatResponse( 1009 | content: $result, 1010 | finishReason: $finishReason->value, 1011 | ), 1012 | $finishReason === FinishReason::ToolCalls => new ToolCalledResponse( 1013 | content: $result, 1014 | tools: \array_values($toolCalls), 1015 | finishReason: $finishReason->value, 1016 | ), 1017 | $finishReason === FinishReason::Length => throw new LimitExceededException(), 1018 | $finishReason === FinishReason::Timeout => throw new TimeoutException(), 1019 | $finishReason === FinishReason::Limit => throw new RateLimitException(), 1020 | }; 1021 | } 1022 | } 1023 | //vendor/llm-agents/openai-client/src/Parsers 1024 | namespace LLM\Agents\OpenAI\Client\Parsers; 1025 | use LLM\Agents\OpenAI\Client\Exception\LimitExceededException; 1026 | use LLM\Agents\OpenAI\Client\Exception\RateLimitException; 1027 | use LLM\Agents\OpenAI\Client\Exception\TimeoutException; 1028 | use LLM\Agents\OpenAI\Client\StreamChunkCallbackInterface; 1029 | use LLM\Agents\LLM\Response\Response; 1030 | use OpenAI\Contracts\ResponseStreamContract; 1031 | interface ParserInterface 1032 | { 1033 | /** 1034 | * @throws LimitExceededException 1035 | * @throws RateLimitException 1036 | * @throws TimeoutException 1037 | */ 1038 | public function parse(ResponseStreamContract $stream, ?StreamChunkCallbackInterface $callback = null): Response; 1039 | } 1040 | //vendor/llm-agents/json-schema-mapper/src/Integration/Laravel 1041 | namespace LLM\Agents\JsonSchema\Mapper\Integration\Laravel; 1042 | use CuyZ\Valinor\Cache\FileSystemCache; 1043 | use CuyZ\Valinor\Mapper\TreeMapper; 1044 | use Illuminate\Contracts\Foundation\Application; 1045 | use Illuminate\Support\ServiceProvider; 1046 | use LLM\Agents\JsonSchema\Mapper\MapperBuilder; 1047 | use LLM\Agents\JsonSchema\Mapper\SchemaMapper; 1048 | use LLM\Agents\Tool\SchemaMapperInterface; 1049 | final class SchemaMapperServiceProvider extends ServiceProvider 1050 | { 1051 | public function register(): void 1052 | { 1053 | $this->app->singleton( 1054 | SchemaMapperInterface::class, 1055 | SchemaMapper::class, 1056 | ); 1057 | $this->app->singleton( 1058 | TreeMapper::class, 1059 | static fn( 1060 | Application $app, 1061 | ) => $app->make(MapperBuilder::class)->build(), 1062 | ); 1063 | $this->app->singleton( 1064 | MapperBuilder::class, 1065 | static fn( 1066 | Application $app, 1067 | ) => new MapperBuilder( 1068 | cache: match (true) { 1069 | $app->environment('prod') => new FileSystemCache( 1070 | cacheDir: $app->storagePath('cache/valinor'), 1071 | ), 1072 | default => null, 1073 | }, 1074 | ), 1075 | ); 1076 | } 1077 | } 1078 | //vendor/llm-agents/json-schema-mapper/src/Integration/Spiral 1079 | namespace LLM\Agents\JsonSchema\Mapper\Integration\Spiral; 1080 | use CuyZ\Valinor\Cache\FileSystemCache; 1081 | use CuyZ\Valinor\Mapper\TreeMapper; 1082 | use LLM\Agents\JsonSchema\Mapper\MapperBuilder; 1083 | use LLM\Agents\JsonSchema\Mapper\SchemaMapper; 1084 | use LLM\Agents\Tool\SchemaMapperInterface; 1085 | use Spiral\Boot\Bootloader\Bootloader; 1086 | use Spiral\Boot\DirectoriesInterface; 1087 | use Spiral\Boot\Environment\AppEnvironment; 1088 | final class SchemaMapperBootloader extends Bootloader 1089 | { 1090 | public function defineSingletons(): array 1091 | { 1092 | return [ 1093 | SchemaMapperInterface::class => SchemaMapper::class, 1094 | TreeMapper::class => static fn( 1095 | MapperBuilder $builder, 1096 | ) => $builder->build(), 1097 | MapperBuilder::class => static fn( 1098 | DirectoriesInterface $dirs, 1099 | AppEnvironment $env, 1100 | ) => new MapperBuilder( 1101 | cache: match ($env) { 1102 | AppEnvironment::Production => new FileSystemCache( 1103 | cacheDir: $dirs->get('runtime') . 'cache/valinor', 1104 | ), 1105 | default => null, 1106 | }, 1107 | ), 1108 | ]; 1109 | } 1110 | } 1111 | //vendor/llm-agents/json-schema-mapper/src 1112 | namespace LLM\Agents\JsonSchema\Mapper; 1113 | use CuyZ\Valinor\Mapper\TreeMapper; 1114 | use CuyZ\Valinor\MapperBuilder as BaseMapperBuilder; 1115 | use Psr\SimpleCache\CacheInterface; 1116 | final readonly class MapperBuilder 1117 | { 1118 | public function __construct( 1119 | private ?CacheInterface $cache = null, 1120 | ) {} 1121 | public function build(): TreeMapper 1122 | { 1123 | $builder = (new BaseMapperBuilder()) 1124 | ->enableFlexibleCasting() 1125 | ->allowPermissiveTypes(); 1126 | if ($this->cache) { 1127 | $builder = $builder->withCache($this->cache); 1128 | } 1129 | return $builder->mapper(); 1130 | } 1131 | } 1132 | //vendor/llm-agents/json-schema-mapper/src 1133 | namespace LLM\Agents\JsonSchema\Mapper; 1134 | use CuyZ\Valinor\Mapper\TreeMapper; 1135 | use LLM\Agents\Tool\SchemaMapperInterface; 1136 | use Spiral\JsonSchemaGenerator\Generator as JsonSchemaGenerator; 1137 | final readonly class SchemaMapper implements SchemaMapperInterface 1138 | { 1139 | public function __construct( 1140 | private JsonSchemaGenerator $generator, 1141 | private TreeMapper $mapper, 1142 | ) {} 1143 | public function toJsonSchema(string $class): array 1144 | { 1145 | if (\json_validate($class)) { 1146 | return \json_decode($class, associative: true); 1147 | } 1148 | if (\class_exists($class)) { 1149 | return $this->generator->generate($class)->jsonSerialize(); 1150 | } 1151 | throw new \InvalidArgumentException(\sprintf('Invalid class or JSON schema provided: %s', $class)); 1152 | } 1153 | /** 1154 | * @template T of object 1155 | * @param class-string|string $class 1156 | * @return T 1157 | */ 1158 | public function toObject(string $json, ?string $class = null): object 1159 | { 1160 | if ($class === null) { 1161 | return \json_decode($json, associative: false); 1162 | } 1163 | return $this->mapper->map($class, \json_decode($json, associative: true)); 1164 | } 1165 | } 1166 | -------------------------------------------------------------------------------- /knowledge-base/what-is-llm.md: -------------------------------------------------------------------------------- 1 | LLM Agents are advanced AI systems that leverage Large Language Models (LLMs) to understand, generate, and respond to 2 | human language in a contextual manner. They are capable of maintaining conversational threads, adjusting tones, and 3 | performing complex tasks like problem-solving, content creation, language translation, and more. Their use cases span 4 | across industries such as customer service, education, and healthcare. 5 | 6 | Key components of LLM Agents: 7 | 8 | - **The Core**: Central processing unit that interprets inputs, applies reasoning, and decides actions. 9 | - **Memory**: Stores interactions and data, enabling personalized and context-aware responses. 10 | - **Tools**: Specialized workflows used for executing tasks such as querying information, coding, or performing complex 11 | functions. 12 | - **Planning Module**: Enables strategic thinking, planning complex tasks, and optimizing long-term objectives. 13 | 14 | The architecture includes: 15 | 16 | - **LLM**: Neural network model that generates human-like text. 17 | - **Integration Layer**: Allows communication with external systems (APIs, databases). 18 | - **Input and Output Processing**: Enhances understanding through additional steps like sentiment analysis. 19 | - **Ethical and Safety Layers**: Ensure appropriate, aligned, and safe responses. 20 | - **User Interface**: Facilitates interaction through text, voice, or other mediums. 21 | 22 | LLM Agents, though powerful, face limitations in understanding human emotions and are susceptible to risks such as 23 | misinformation and bias. Prompts from users drive their actions, and with autonomous capabilities, they help reduce 24 | menial tasks and solve complex problems efficiently. 25 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | tests/Unit 18 | 19 | 20 | tests/Feature 21 | 22 | 23 | 24 | 25 | app/src 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/App/TestKernel.php: -------------------------------------------------------------------------------- 1 | persist(...$entities); 23 | } 24 | 25 | protected function tearDown(): void 26 | { 27 | parent::tearDown(); 28 | 29 | $this->cleanIdentityMap(); 30 | $this->getCurrentDatabaseDriver()->disconnect(); 31 | } 32 | 33 | public function persist(object ...$entity): void 34 | { 35 | $em = $this->getEntityManager(); 36 | foreach ($entity as $e) { 37 | $em->persist($e); 38 | } 39 | $em->run(); 40 | } 41 | 42 | /** 43 | * @template T of object 44 | * @param T $entity 45 | * @return T 46 | */ 47 | public function refreshEntity(object $entity, string $pkField = 'uuid'): object 48 | { 49 | return $this->getRepositoryFor($entity)->findByPK($entity->{$pkField}); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Feature/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-agents-php/sample-app/3fe05382a91bad58c3d1ffe525e8bf5546103b56/tests/Feature/.gitignore -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | defineDirectories( 23 | $this->rootDirectory(), 24 | ), 25 | container: $container, 26 | ); 27 | } 28 | 29 | protected function tearDown(): void 30 | { 31 | // Uncomment this line if you want to clean up runtime directory. 32 | // $this->cleanUpRuntimeDirectory(); 33 | } 34 | 35 | public function rootDirectory(): string 36 | { 37 | return __DIR__ . '/..'; 38 | } 39 | 40 | public function defineDirectories(string $root): array 41 | { 42 | return [ 43 | 'root' => $root, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/Application/Entity/JsonTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Json::class, $json); 18 | $this->assertEquals(['key' => 'value'], $json->data); 19 | } 20 | 21 | public function testConstructor(): void 22 | { 23 | $data = ['foo' => 'bar']; 24 | $json = new Json($data); 25 | 26 | $this->assertEquals($data, $json->data); 27 | } 28 | 29 | public function testTypecastWithEmptyValue(): void 30 | { 31 | $json = Json::typecast(''); 32 | 33 | $this->assertInstanceOf(Json::class, $json); 34 | $this->assertEquals([], $json->data); 35 | } 36 | 37 | public function testTypecastWithValidJson(): void 38 | { 39 | $jsonString = '{"name":"John","age":30}'; 40 | $json = Json::typecast($jsonString); 41 | 42 | $this->assertInstanceOf(Json::class, $json); 43 | $this->assertEquals(['name' => 'John', 'age' => 30], $json->data); 44 | } 45 | 46 | public function testTypecastWithInvalidJson(): void 47 | { 48 | $this->expectException(\InvalidArgumentException::class); 49 | Json::typecast('{invalid json}'); 50 | } 51 | 52 | public function testJsonSerializeWithArray(): void 53 | { 54 | $data = ['a' => 1, 'b' => 2]; 55 | $json = new Json($data); 56 | 57 | $this->assertEquals($data, $json->jsonSerialize()); 58 | } 59 | 60 | public function testJsonSerializeWithJsonSerializable(): void 61 | { 62 | $jsonSerializable = new class implements \JsonSerializable { 63 | public function jsonSerialize(): array 64 | { 65 | return ['x' => 'y']; 66 | } 67 | }; 68 | 69 | $json = new Json($jsonSerializable); 70 | 71 | $this->assertEquals(['x' => 'y'], $json->jsonSerialize()); 72 | } 73 | 74 | public function testToString(): void 75 | { 76 | $data = ['hello' => 'world']; 77 | $json = new Json($data); 78 | 79 | $this->assertEquals('{"hello":"world"}', (string) $json); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Unit/Application/Entity/UuidTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Uuid::class, $uuid); 16 | $this->assertTrue(\Ramsey\Uuid\Uuid::isValid((string) $uuid)); 17 | } 18 | 19 | public function testFromString(): void 20 | { 21 | $uuidString = 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6'; 22 | $uuid = Uuid::fromString($uuidString); 23 | $this->assertInstanceOf(Uuid::class, $uuid); 24 | $this->assertEquals($uuidString, (string) $uuid); 25 | } 26 | 27 | public function testTypecast(): void 28 | { 29 | $uuidString = 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6'; 30 | $uuid = Uuid::typecast($uuidString); 31 | $this->assertInstanceOf(Uuid::class, $uuid); 32 | $this->assertEquals($uuidString, (string) $uuid); 33 | } 34 | 35 | public function testEquals(): void 36 | { 37 | $uuid1 = Uuid::generate(); 38 | $uuid2 = Uuid::fromString((string) $uuid1); 39 | $uuid3 = Uuid::generate(); 40 | 41 | $this->assertTrue($uuid1->equals($uuid2)); 42 | $this->assertTrue($uuid2->equals($uuid1)); 43 | $this->assertFalse($uuid1->equals($uuid3)); 44 | } 45 | 46 | public function testToString(): void 47 | { 48 | $uuid = Uuid::generate(); 49 | $this->assertIsString((string) $uuid); 50 | $this->assertTrue(\Ramsey\Uuid\Uuid::isValid((string) $uuid)); 51 | } 52 | 53 | public function testJsonSerialize(): void 54 | { 55 | $uuid = Uuid::generate(); 56 | $json = \json_encode($uuid); 57 | $this->assertIsString($json); 58 | $this->assertTrue(\Ramsey\Uuid\Uuid::isValid(\json_decode($json))); 59 | } 60 | 61 | public function testInvalidUuidString(): void 62 | { 63 | $this->expectException(\InvalidArgumentException::class); 64 | Uuid::fromString('invalid-uuid'); 65 | } 66 | } 67 | --------------------------------------------------------------------------------