Skip to content

Commit 2bd187b

Browse files
Merge pull request #2 from dedrisproject/codex/create-php-daemon-as-api
Add Windows launcher and Postman collection for PHP daemon
2 parents d7439b2 + 38cd692 commit 2bd187b

File tree

6 files changed

+591
-0
lines changed

6 files changed

+591
-0
lines changed

php/PromptExecutor.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DedrisGenAI\PhpApi;
6+
7+
use DateTimeImmutable;
8+
use RuntimeException;
9+
10+
/**
11+
* Small helper responsible for executing prompts received by the daemon.
12+
*/
13+
class PromptExecutor
14+
{
15+
public const DEFAULT_LOG = __DIR__ . '/prompt_daemon.log';
16+
17+
/** @var string|null */
18+
private $logFile;
19+
20+
public function __construct(?string $logFile = self::DEFAULT_LOG)
21+
{
22+
$this->logFile = $logFile;
23+
}
24+
25+
/**
26+
* Execute the provided prompt and return a structured response.
27+
*
28+
* The current implementation simply echoes the prompt back and returns
29+
* metadata about when the execution occurred. Replace the body of this
30+
* method with the actual logic that should run when a prompt is received.
31+
*/
32+
public function execute(string $prompt): array
33+
{
34+
if ($prompt === '') {
35+
throw new RuntimeException('Prompt must not be empty.');
36+
}
37+
38+
$timestamp = (new DateTimeImmutable('now'))->format(DATE_ATOM);
39+
40+
$result = [
41+
'prompt' => $prompt,
42+
'response' => sprintf('Prompt processed at %s', $timestamp),
43+
'timestamp' => $timestamp,
44+
];
45+
46+
$this->logExecution($prompt, $timestamp);
47+
48+
return $result;
49+
}
50+
51+
private function logExecution(string $prompt, string $timestamp): void
52+
{
53+
if ($this->logFile === null) {
54+
return;
55+
}
56+
57+
$entry = sprintf("[%s] prompt: %s\n", $timestamp, $prompt);
58+
file_put_contents($this->logFile, $entry, FILE_APPEND | LOCK_EX);
59+
}
60+
}

php/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# PHP Prompt API Daemon
2+
3+
This folder contains a lightweight PHP daemon that exposes a REST-style API
4+
for executing prompts. The daemon can be started with a regular PHP CLI
5+
process and is intended to run continuously in the background.
6+
7+
## Files
8+
9+
- `prompt_api_daemon.php`: the entry point that starts the TCP server and
10+
handles HTTP requests.
11+
- `PromptExecutor.php`: small helper that performs the actual prompt
12+
execution. Replace the logic inside `execute()` with your application
13+
specific code.
14+
- `prompt_client.html`: semplice interfaccia web che invia richieste AJAX
15+
all'endpoint `/execute` del demone.
16+
17+
## Running the daemon
18+
19+
```bash
20+
php php/prompt_api_daemon.php
21+
```
22+
23+
Environment variables:
24+
25+
- `PROMPT_API_HOST` (default `127.0.0.1`): interface to bind.
26+
- `PROMPT_API_PORT` (default `8080`): port the daemon listens on.
27+
28+
### Windows quick start (port 9001)
29+
30+
For a ready-to-run configuration that binds the daemon to port `9001`, use the
31+
included batch file:
32+
33+
```bat
34+
php\start_prompt_daemon_9001.bat
35+
```
36+
37+
The script sets the appropriate environment variables and launches the daemon
38+
from the repository root or any other directory.
39+
40+
## API
41+
42+
### `POST /execute`
43+
44+
Send a JSON payload containing a `prompt` field. The daemon passes the prompt
45+
to the `PromptExecutor` and returns the structured result.
46+
47+
```bash
48+
curl -X POST "http://127.0.0.1:9001/execute" \
49+
-H "Content-Type: application/json" \
50+
-d '{"prompt": "Hello daemon"}'
51+
```
52+
53+
Example response:
54+
55+
```json
56+
{
57+
"status": "success",
58+
"result": {
59+
"prompt": "Hello daemon",
60+
"response": "Prompt processed at 2024-05-01T12:00:00+00:00",
61+
"timestamp": "2024-05-01T12:00:00+00:00"
62+
}
63+
}
64+
```
65+
66+
### `GET /health`
67+
68+
Simple health check endpoint returning `{ "status": "ok" }` to indicate that
69+
the daemon is running.
70+
71+
## Postman collection
72+
73+
Import `php/prompt_api_daemon.postman_collection.json` into Postman for
74+
pre-configured requests targeting the daemon on port `9001`. The collection
75+
includes:
76+
77+
- **Health Check** – verifies that the service is running.
78+
- **Execute Prompt** – sends a sample prompt payload and displays the response.
79+
80+
## Interfaccia web con AJAX
81+
82+
Per testare rapidamente l'API dal browser è disponibile la pagina
83+
`prompt_client.html`. I passaggi consigliati sono:
84+
85+
1. Avvia il demone (es. con `php php/prompt_api_daemon.php` oppure tramite lo
86+
script batch su Windows) assicurandoti che sia in ascolto su `127.0.0.1:9001`.
87+
2. Avvia un semplice web server per servire i file statici, ad esempio:
88+
89+
```bash
90+
php -S 127.0.0.1:8000 -t php
91+
```
92+
93+
3. Apri il browser su `http://127.0.0.1:8000/prompt_client.html`, inserisci il
94+
prompt nel form e premi "Invia prompt". La risposta JSON del demone verrà
95+
mostrata all'interno della pagina.
96+
97+
Il demone restituisce intestazioni CORS permissive (`Access-Control-Allow-*`)
98+
in modo da accettare chiamate AJAX anche da domini o porte differenti durante
99+
lo sviluppo.

php/prompt_api_daemon.php

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use DedrisGenAI\PhpApi\PromptExecutor;
6+
7+
require __DIR__ . '/PromptExecutor.php';
8+
9+
$host = getenv('PROMPT_API_HOST') ?: '127.0.0.1';
10+
$port = (int) (getenv('PROMPT_API_PORT') ?: 8080);
11+
12+
$endpoint = sprintf('tcp://%s:%d', $host, $port);
13+
14+
$server = @stream_socket_server($endpoint, $errno, $errstr);
15+
if ($server === false) {
16+
fwrite(STDERR, sprintf("Failed to start server on %s: [%d] %s\n", $endpoint, $errno, $errstr));
17+
exit(1);
18+
}
19+
20+
stream_set_blocking($server, true);
21+
22+
$shouldRun = true;
23+
24+
$signalHandler = static function () use (&$shouldRun): void {
25+
$shouldRun = false;
26+
};
27+
28+
if (function_exists('pcntl_signal')) {
29+
pcntl_signal(SIGINT, $signalHandler);
30+
pcntl_signal(SIGTERM, $signalHandler);
31+
}
32+
33+
$executor = new PromptExecutor();
34+
35+
fwrite(STDOUT, sprintf("Prompt API daemon listening on %s\n", $endpoint));
36+
37+
while ($shouldRun) {
38+
if (function_exists('pcntl_signal_dispatch')) {
39+
pcntl_signal_dispatch();
40+
}
41+
42+
$client = @stream_socket_accept($server, 1);
43+
if ($client === false) {
44+
continue;
45+
}
46+
47+
handleClient($client, $executor);
48+
}
49+
50+
fclose($server);
51+
52+
function handleClient($client, PromptExecutor $executor): void
53+
{
54+
stream_set_timeout($client, 5);
55+
56+
$requestLine = fgets($client);
57+
if ($requestLine === false) {
58+
fclose($client);
59+
return;
60+
}
61+
62+
$requestLine = trim($requestLine);
63+
if ($requestLine === '') {
64+
fclose($client);
65+
return;
66+
}
67+
68+
$headers = [];
69+
while (($line = fgets($client)) !== false) {
70+
$line = rtrim($line, "\r\n");
71+
if ($line === '') {
72+
break;
73+
}
74+
$parts = explode(':', $line, 2);
75+
if (count($parts) === 2) {
76+
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
77+
}
78+
}
79+
80+
$contentLength = isset($headers['content-length']) ? (int) $headers['content-length'] : 0;
81+
$body = '';
82+
if ($contentLength > 0) {
83+
$body = stream_get_contents($client, $contentLength) ?: '';
84+
}
85+
86+
[$method, $path] = parseRequestLine($requestLine);
87+
88+
if ($method === 'GET' && $path === '/health') {
89+
respondJson($client, 200, ['status' => 'ok']);
90+
return;
91+
}
92+
93+
if ($method === 'OPTIONS' && $path === '/execute') {
94+
respondNoContent($client);
95+
return;
96+
}
97+
98+
if ($method !== 'POST' || $path !== '/execute') {
99+
respondJson($client, 404, ['error' => 'Not found']);
100+
return;
101+
}
102+
103+
$data = json_decode($body, true);
104+
if (!is_array($data) || !array_key_exists('prompt', $data)) {
105+
respondJson($client, 400, ['error' => 'Invalid payload: expected JSON with a "prompt" field.']);
106+
return;
107+
}
108+
109+
try {
110+
$result = $executor->execute((string) $data['prompt']);
111+
} catch (Throwable $throwable) {
112+
respondJson($client, 500, [
113+
'error' => 'Failed to execute prompt.',
114+
'details' => $throwable->getMessage(),
115+
]);
116+
return;
117+
}
118+
119+
respondJson($client, 200, [
120+
'status' => 'success',
121+
'result' => $result,
122+
]);
123+
}
124+
125+
function parseRequestLine(string $requestLine): array
126+
{
127+
$parts = explode(' ', $requestLine);
128+
$method = strtoupper($parts[0] ?? '');
129+
$path = $parts[1] ?? '/';
130+
131+
return [$method, $path];
132+
}
133+
134+
function respondJson($client, int $statusCode, array $payload): void
135+
{
136+
$statusText = match ($statusCode) {
137+
200 => 'OK',
138+
400 => 'Bad Request',
139+
404 => 'Not Found',
140+
500 => 'Internal Server Error',
141+
default => 'OK',
142+
};
143+
144+
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
145+
if ($body === false) {
146+
$body = json_encode(['error' => 'Failed to encode response.']);
147+
}
148+
149+
$headers = [
150+
sprintf('HTTP/1.1 %d %s', $statusCode, $statusText),
151+
'Content-Type: application/json; charset=utf-8',
152+
'Connection: close',
153+
'Content-Length: ' . strlen((string) $body),
154+
'Access-Control-Allow-Origin: *',
155+
'Access-Control-Allow-Headers: Content-Type',
156+
'Access-Control-Allow-Methods: GET, POST, OPTIONS',
157+
];
158+
159+
fwrite($client, implode("\r\n", $headers) . "\r\n\r\n" . $body);
160+
fclose($client);
161+
}
162+
163+
function respondNoContent($client): void
164+
{
165+
$headers = [
166+
'HTTP/1.1 204 No Content',
167+
'Connection: close',
168+
'Access-Control-Allow-Origin: *',
169+
'Access-Control-Allow-Headers: Content-Type',
170+
'Access-Control-Allow-Methods: GET, POST, OPTIONS',
171+
];
172+
173+
fwrite($client, implode("\r\n", $headers) . "\r\n\r\n");
174+
fclose($client);
175+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"info": {
3+
"_postman_id": "6d6b1f55-1b0d-4f47-996f-48c17fc5793e",
4+
"name": "PHP Prompt API Daemon",
5+
"description": "Quick-start requests for interacting with the PHP Prompt API daemon listening on port 9001.",
6+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7+
},
8+
"item": [
9+
{
10+
"name": "Health Check",
11+
"request": {
12+
"method": "GET",
13+
"header": [],
14+
"url": {
15+
"raw": "{{baseUrl}}/health",
16+
"host": [
17+
"{{baseUrl}}"
18+
],
19+
"path": [
20+
"health"
21+
]
22+
}
23+
},
24+
"response": []
25+
},
26+
{
27+
"name": "Execute Prompt",
28+
"request": {
29+
"method": "POST",
30+
"header": [
31+
{
32+
"key": "Content-Type",
33+
"value": "application/json"
34+
}
35+
],
36+
"body": {
37+
"mode": "raw",
38+
"raw": "{\n \"prompt\": \"Hello daemon\"\n}"
39+
},
40+
"url": {
41+
"raw": "{{baseUrl}}/execute",
42+
"host": [
43+
"{{baseUrl}}"
44+
],
45+
"path": [
46+
"execute"
47+
]
48+
}
49+
},
50+
"response": []
51+
}
52+
],
53+
"variable": [
54+
{
55+
"key": "baseUrl",
56+
"value": "http://127.0.0.1:9001"
57+
}
58+
]
59+
}

0 commit comments

Comments
 (0)