create-prisma-php-app 4.0.0-alpha.2 → 4.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.htaccess +54 -41
- package/dist/bootstrap.php +143 -98
- package/dist/index.js +264 -99
- package/dist/settings/auto-swagger-docs.ts +196 -95
- package/dist/settings/bs-config.ts +56 -58
- package/dist/settings/files-list.json +1 -1
- package/dist/settings/restart-mcp.ts +58 -0
- package/dist/settings/restart-websocket.ts +51 -45
- package/dist/settings/utils.ts +240 -0
- package/dist/src/Lib/AI/ChatGPTClient.php +147 -0
- package/dist/src/Lib/Auth/Auth.php +544 -0
- package/dist/src/Lib/Auth/AuthConfig.php +89 -0
- package/dist/src/Lib/CacheHandler.php +121 -0
- package/dist/src/Lib/ErrorHandler.php +322 -0
- package/dist/src/Lib/FileManager/UploadFile.php +383 -0
- package/dist/src/Lib/Headers/Boom.php +192 -0
- package/dist/src/Lib/IncludeTracker.php +59 -0
- package/dist/src/Lib/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/MainLayout.php +230 -0
- package/dist/src/Lib/Middleware/AuthMiddleware.php +154 -0
- package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
- package/dist/src/Lib/PHPMailer/Mailer.php +169 -0
- package/dist/src/Lib/PHPX/Exceptions/ComponentValidationException.php +49 -0
- package/dist/src/Lib/PHPX/Fragment.php +32 -0
- package/dist/src/Lib/PHPX/IPHPX.php +22 -0
- package/dist/src/Lib/PHPX/PHPX.php +287 -0
- package/dist/src/Lib/PHPX/TemplateCompiler.php +641 -0
- package/dist/src/Lib/PHPX/TwMerge.php +346 -0
- package/dist/src/Lib/PHPX/TypeCoercer.php +490 -0
- package/dist/src/Lib/PartialRenderer.php +40 -0
- package/dist/src/Lib/PrismaPHPSettings.php +181 -0
- package/dist/src/Lib/Request.php +479 -0
- package/dist/src/Lib/Security/RateLimiter.php +33 -0
- package/dist/src/Lib/Set.php +102 -0
- package/dist/src/Lib/StateManager.php +127 -0
- package/dist/src/Lib/Validator.php +752 -0
- package/dist/src/{Websocket → Lib/Websocket}/ConnectionManager.php +1 -1
- package/dist/src/Lib/Websocket/websocket-server.php +118 -0
- package/dist/src/app/error.php +1 -1
- package/dist/src/app/index.php +24 -5
- package/dist/src/app/js/index.js +1 -1
- package/dist/src/app/layout.php +2 -2
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
- package/dist/src/app/assets/images/prisma-php-black.svg +0 -6
- package/dist/websocket-server.php +0 -22
- package/vendor/autoload.php +0 -25
- package/vendor/composer/ClassLoader.php +0 -579
- package/vendor/composer/InstalledVersions.php +0 -359
- package/vendor/composer/LICENSE +0 -21
- package/vendor/composer/autoload_classmap.php +0 -10
- package/vendor/composer/autoload_namespaces.php +0 -9
- package/vendor/composer/autoload_psr4.php +0 -10
- package/vendor/composer/autoload_real.php +0 -38
- package/vendor/composer/autoload_static.php +0 -25
- package/vendor/composer/installed.json +0 -825
- package/vendor/composer/installed.php +0 -132
- package/vendor/composer/platform_check.php +0 -26
|
@@ -1,46 +1,52 @@
|
|
|
1
|
-
import { spawn, ChildProcess } from "child_process";
|
|
2
1
|
import { join } from "path";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
2
|
+
import {
|
|
3
|
+
createRestartableProcess,
|
|
4
|
+
createSrcWatcher,
|
|
5
|
+
DebouncedWorker,
|
|
6
|
+
DEFAULT_AWF,
|
|
7
|
+
onExit,
|
|
8
|
+
} from "./utils.js";
|
|
9
|
+
|
|
10
|
+
// Config
|
|
11
|
+
const phpPath = process.env.PHP_PATH ?? "php";
|
|
12
|
+
const SRC_DIR = join(process.cwd(), "src");
|
|
13
|
+
const serverScriptPath = join(
|
|
14
|
+
SRC_DIR,
|
|
15
|
+
"Lib",
|
|
16
|
+
"Websocket",
|
|
17
|
+
"websocket-server.php"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Restartable WS server
|
|
21
|
+
const ws = createRestartableProcess({
|
|
22
|
+
name: "WebSocket",
|
|
23
|
+
cmd: phpPath,
|
|
24
|
+
args: [serverScriptPath],
|
|
25
|
+
windowsKillTree: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
ws.start();
|
|
29
|
+
|
|
30
|
+
// Debounced restarter
|
|
31
|
+
const restarter = new DebouncedWorker(
|
|
32
|
+
async () => {
|
|
33
|
+
await ws.restart("file change");
|
|
34
|
+
},
|
|
35
|
+
400,
|
|
36
|
+
"ws-restart"
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Watch ./src recursively; restart on code/data file changes
|
|
40
|
+
createSrcWatcher(SRC_DIR, {
|
|
41
|
+
exts: [".php", ".ts", ".js", ".json"],
|
|
42
|
+
onEvent: (ev, _abs, rel) => restarter.schedule(`${ev}: ${rel}`),
|
|
43
|
+
awaitWriteFinish: DEFAULT_AWF,
|
|
44
|
+
logPrefix: "WS watch",
|
|
45
|
+
usePolling: true,
|
|
46
|
+
interval: 1000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Graceful shutdown
|
|
50
|
+
onExit(async () => {
|
|
51
|
+
await ws.stop();
|
|
52
|
+
});
|
package/dist/settings/utils.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { fileURLToPath } from "url";
|
|
2
2
|
import { dirname } from "path";
|
|
3
|
+
import chokidar, { FSWatcher } from "chokidar";
|
|
4
|
+
import { spawn, ChildProcess, execFile } from "child_process";
|
|
5
|
+
import { relative } from "path";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Retrieves the file metadata including the filename and directory name.
|
|
@@ -12,3 +15,240 @@ export function getFileMeta() {
|
|
|
12
15
|
const __dirname = dirname(__filename);
|
|
13
16
|
return { __filename, __dirname };
|
|
14
17
|
}
|
|
18
|
+
|
|
19
|
+
export type WatchEvent = "add" | "addDir" | "change" | "unlink" | "unlinkDir";
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_IGNORES: (string | RegExp)[] = [
|
|
22
|
+
/(^|[\/\\])\../, // dotfiles
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/vendor/**",
|
|
25
|
+
"**/dist/**",
|
|
26
|
+
"**/build/**",
|
|
27
|
+
"**/.cache/**",
|
|
28
|
+
"**/*.log",
|
|
29
|
+
"**/*.tmp",
|
|
30
|
+
"**/*.swp",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_AWF = { stabilityThreshold: 300, pollInterval: 100 };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a chokidar watcher for a given root. Optionally filter by file extensions.
|
|
37
|
+
*/
|
|
38
|
+
export function createSrcWatcher(
|
|
39
|
+
root: string,
|
|
40
|
+
opts: {
|
|
41
|
+
exts?: string[]; // e.g. ['.php','.ts']
|
|
42
|
+
onEvent: (event: WatchEvent, absPath: string, relPath: string) => void;
|
|
43
|
+
ignored?: (string | RegExp)[];
|
|
44
|
+
awaitWriteFinish?: { stabilityThreshold: number; pollInterval: number };
|
|
45
|
+
logPrefix?: string;
|
|
46
|
+
usePolling?: boolean;
|
|
47
|
+
interval?: number;
|
|
48
|
+
}
|
|
49
|
+
): FSWatcher {
|
|
50
|
+
const {
|
|
51
|
+
exts,
|
|
52
|
+
onEvent,
|
|
53
|
+
ignored = DEFAULT_IGNORES,
|
|
54
|
+
awaitWriteFinish = DEFAULT_AWF,
|
|
55
|
+
logPrefix = "watch",
|
|
56
|
+
usePolling = true,
|
|
57
|
+
} = opts;
|
|
58
|
+
|
|
59
|
+
const watcher = chokidar.watch(root, {
|
|
60
|
+
ignoreInitial: true,
|
|
61
|
+
persistent: true,
|
|
62
|
+
ignored,
|
|
63
|
+
awaitWriteFinish,
|
|
64
|
+
usePolling,
|
|
65
|
+
interval: opts.interval ?? 1000,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
watcher
|
|
69
|
+
.on("ready", () => {
|
|
70
|
+
console.log(`[${logPrefix}] Watching ${root.replace(/\\/g, "/")}/**/*`);
|
|
71
|
+
})
|
|
72
|
+
.on("all", (event: WatchEvent, filePath: string) => {
|
|
73
|
+
// Optional extension filter
|
|
74
|
+
if (exts && exts.length > 0) {
|
|
75
|
+
const ok = exts.some((ext) => filePath.endsWith(ext));
|
|
76
|
+
if (!ok) return;
|
|
77
|
+
}
|
|
78
|
+
const rel = relative(root, filePath).replace(/\\/g, "/");
|
|
79
|
+
if (event === "add" || event === "change" || event === "unlink") {
|
|
80
|
+
onEvent(event, filePath, rel);
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.on("error", (err) => console.error(`[${logPrefix}] Error:`, err));
|
|
84
|
+
|
|
85
|
+
return watcher;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Debounced worker that ensures only one run at a time; extra runs get queued once.
|
|
90
|
+
*/
|
|
91
|
+
export class DebouncedWorker {
|
|
92
|
+
private timer: NodeJS.Timeout | null = null;
|
|
93
|
+
private running = false;
|
|
94
|
+
private queued = false;
|
|
95
|
+
|
|
96
|
+
constructor(
|
|
97
|
+
private work: () => Promise<void> | void,
|
|
98
|
+
private debounceMs = 350,
|
|
99
|
+
private name = "worker"
|
|
100
|
+
) {}
|
|
101
|
+
|
|
102
|
+
schedule(reason?: string) {
|
|
103
|
+
if (reason) console.log(`[${this.name}] ${reason} → scheduled`);
|
|
104
|
+
if (this.timer) clearTimeout(this.timer);
|
|
105
|
+
this.timer = setTimeout(() => {
|
|
106
|
+
this.timer = null;
|
|
107
|
+
this.runNow().catch(() => {});
|
|
108
|
+
}, this.debounceMs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async runNow() {
|
|
112
|
+
if (this.running) {
|
|
113
|
+
this.queued = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.running = true;
|
|
117
|
+
try {
|
|
118
|
+
await this.work();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`[${this.name}] error:`, err);
|
|
121
|
+
} finally {
|
|
122
|
+
this.running = false;
|
|
123
|
+
if (this.queued) {
|
|
124
|
+
this.queued = false;
|
|
125
|
+
this.runNow().catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Cross-platform restartable process.
|
|
133
|
+
*/
|
|
134
|
+
export function createRestartableProcess(spec: {
|
|
135
|
+
name: string;
|
|
136
|
+
cmd: string;
|
|
137
|
+
args?: string[];
|
|
138
|
+
stdio?: "inherit" | [any, any, any];
|
|
139
|
+
gracefulSignal?: NodeJS.Signals;
|
|
140
|
+
forceKillAfterMs?: number;
|
|
141
|
+
windowsKillTree?: boolean;
|
|
142
|
+
onStdout?: (buf: Buffer) => void;
|
|
143
|
+
onStderr?: (buf: Buffer) => void;
|
|
144
|
+
}) {
|
|
145
|
+
const {
|
|
146
|
+
name,
|
|
147
|
+
cmd,
|
|
148
|
+
args = [],
|
|
149
|
+
stdio = ["ignore", "pipe", "pipe"],
|
|
150
|
+
gracefulSignal = "SIGINT",
|
|
151
|
+
forceKillAfterMs = 2000,
|
|
152
|
+
windowsKillTree = true,
|
|
153
|
+
onStdout,
|
|
154
|
+
onStderr,
|
|
155
|
+
} = spec;
|
|
156
|
+
|
|
157
|
+
let child: ChildProcess | null = null;
|
|
158
|
+
|
|
159
|
+
function start() {
|
|
160
|
+
console.log(`[${name}] Starting: ${cmd} ${args.join(" ")}`.trim());
|
|
161
|
+
child = spawn(cmd, args, { stdio, windowsHide: true });
|
|
162
|
+
|
|
163
|
+
child.stdout?.on("data", (buf: Buffer) => {
|
|
164
|
+
if (onStdout) onStdout(buf);
|
|
165
|
+
else process.stdout.write(`[${name}] ${buf.toString()}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
child.stderr?.on("data", (buf: Buffer) => {
|
|
169
|
+
if (onStderr) onStderr(buf);
|
|
170
|
+
else process.stderr.write(`[${name}:err] ${buf.toString()}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
child.on("close", (code) => {
|
|
174
|
+
console.log(`[${name}] Exited with code ${code}`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
child.on("error", (err) => {
|
|
178
|
+
console.error(`[${name}] Failed to start:`, err);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return child;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function killOnWindows(pid: number): Promise<void> {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const cp = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
|
|
187
|
+
resolve()
|
|
188
|
+
);
|
|
189
|
+
cp.on("error", () => resolve());
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function stop(): Promise<void> {
|
|
194
|
+
if (!child || child.killed) return;
|
|
195
|
+
const pid = child.pid!;
|
|
196
|
+
console.log(`[${name}] Stopping…`);
|
|
197
|
+
|
|
198
|
+
if (process.platform === "win32" && windowsKillTree) {
|
|
199
|
+
await killOnWindows(pid);
|
|
200
|
+
child = null;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await new Promise<void>((resolve) => {
|
|
205
|
+
const done = () => resolve();
|
|
206
|
+
child!.once("close", done).once("exit", done).once("disconnect", done);
|
|
207
|
+
try {
|
|
208
|
+
child!.kill(gracefulSignal);
|
|
209
|
+
} catch {
|
|
210
|
+
resolve();
|
|
211
|
+
}
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (child && !child.killed) {
|
|
214
|
+
try {
|
|
215
|
+
process.kill(pid, "SIGKILL");
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
}, forceKillAfterMs);
|
|
219
|
+
});
|
|
220
|
+
child = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function restart(reason?: string) {
|
|
224
|
+
if (reason) console.log(`[${name}] Restart requested: ${reason}`);
|
|
225
|
+
await stop();
|
|
226
|
+
return start();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getChild() {
|
|
230
|
+
return child;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { start, stop, restart, getChild };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Register shutdown cleanup callbacks.
|
|
238
|
+
*/
|
|
239
|
+
export function onExit(fn: () => Promise<void> | void) {
|
|
240
|
+
const wrap = (sig: string) => async () => {
|
|
241
|
+
console.log(`[proc] Received ${sig}, shutting down…`);
|
|
242
|
+
try {
|
|
243
|
+
await fn();
|
|
244
|
+
} finally {
|
|
245
|
+
process.exit(0);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
process.on("SIGINT", wrap("SIGINT"));
|
|
249
|
+
process.on("SIGTERM", wrap("SIGTERM"));
|
|
250
|
+
process.on("uncaughtException", async (err) => {
|
|
251
|
+
console.error("[proc] Uncaught exception:", err);
|
|
252
|
+
await wrap("uncaughtException")();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\AI;
|
|
6
|
+
|
|
7
|
+
use GuzzleHttp\Client;
|
|
8
|
+
use GuzzleHttp\Exception\RequestException;
|
|
9
|
+
use Lib\Validator;
|
|
10
|
+
use RuntimeException;
|
|
11
|
+
|
|
12
|
+
class ChatGPTClient
|
|
13
|
+
{
|
|
14
|
+
private Client $client;
|
|
15
|
+
private string $apiUrl = '';
|
|
16
|
+
private string $apiKey = '';
|
|
17
|
+
private array $cache = [];
|
|
18
|
+
|
|
19
|
+
public function __construct(?Client $client = null)
|
|
20
|
+
{
|
|
21
|
+
// Initialize the Guzzle HTTP client, allowing for dependency injection
|
|
22
|
+
$this->client = $client ?: new Client();
|
|
23
|
+
|
|
24
|
+
// API URL for chat completions
|
|
25
|
+
$this->apiUrl = 'https://api.openai.com/v1/chat/completions';
|
|
26
|
+
|
|
27
|
+
// Get the API key from environment variables (keep this private and secure)
|
|
28
|
+
$this->apiKey = $_ENV['CHATGPT_API_KEY'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Determines the appropriate model based on internal logic.
|
|
33
|
+
*
|
|
34
|
+
* @param array $conversationHistory The conversation history array.
|
|
35
|
+
* @return string The model name to be used.
|
|
36
|
+
*/
|
|
37
|
+
protected function determineModel(array $conversationHistory): string
|
|
38
|
+
{
|
|
39
|
+
$messageCount = count($conversationHistory);
|
|
40
|
+
$totalTokens = array_reduce(
|
|
41
|
+
$conversationHistory,
|
|
42
|
+
fn($carry, $item) => $carry + str_word_count($item['content'] ?? ''),
|
|
43
|
+
0
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// If the conversation is long or complex, use a model with more tokens
|
|
47
|
+
if ($totalTokens > 4000 || $messageCount > 10) {
|
|
48
|
+
return 'gpt-3.5-turbo-16k'; // Use the model with a larger token limit
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Default to the standard model for shorter conversations
|
|
52
|
+
return 'gpt-3.5-turbo';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Formats the conversation history to ensure it is valid.
|
|
57
|
+
*
|
|
58
|
+
* @param array $conversationHistory The conversation history array.
|
|
59
|
+
* @return array The formatted conversation history.
|
|
60
|
+
*/
|
|
61
|
+
protected function formatConversationHistory(array $conversationHistory): array
|
|
62
|
+
{
|
|
63
|
+
$formattedHistory = [];
|
|
64
|
+
foreach ($conversationHistory as $message) {
|
|
65
|
+
if (is_array($message) && isset($message['role'], $message['content']) && Validator::string($message['content'])) {
|
|
66
|
+
$formattedHistory[] = $message;
|
|
67
|
+
} else {
|
|
68
|
+
$formattedHistory[] = ['role' => 'user', 'content' => (string) $message];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return $formattedHistory;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sends a message to the OpenAI API and returns the AI's response as HTML.
|
|
76
|
+
*
|
|
77
|
+
* @param array $conversationHistory The conversation history array containing previous messages.
|
|
78
|
+
* @param string $userMessage The new user message to add to the conversation.
|
|
79
|
+
* @return string The AI-generated HTML response.
|
|
80
|
+
*
|
|
81
|
+
* @throws \InvalidArgumentException If a message in the conversation history is not valid.
|
|
82
|
+
* @throws RuntimeException If the API request fails or returns an unexpected format.
|
|
83
|
+
*/
|
|
84
|
+
public function sendMessage(array $conversationHistory, string $userMessage): string
|
|
85
|
+
{
|
|
86
|
+
if (!Validator::string($userMessage)) {
|
|
87
|
+
throw new \InvalidArgumentException("Invalid user message: must be a string.");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Optional: Convert emojis or special patterns in the message
|
|
91
|
+
$userMessage = Validator::emojis($userMessage);
|
|
92
|
+
|
|
93
|
+
// Prepare the conversation, including a system-level instruction to return valid HTML
|
|
94
|
+
$systemInstruction = [
|
|
95
|
+
'role' => 'system',
|
|
96
|
+
'content' => 'You are ChatGPT. Please provide your response in valid HTML format.'
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Format existing history, then prepend the system message
|
|
100
|
+
$formattedHistory = $this->formatConversationHistory($conversationHistory);
|
|
101
|
+
array_unshift($formattedHistory, $systemInstruction);
|
|
102
|
+
|
|
103
|
+
// Append the new user message
|
|
104
|
+
$formattedHistory[] = ['role' => 'user', 'content' => $userMessage];
|
|
105
|
+
|
|
106
|
+
// Check cache
|
|
107
|
+
$cacheKey = md5(serialize($formattedHistory));
|
|
108
|
+
if (isset($this->cache[$cacheKey])) {
|
|
109
|
+
return $this->cache[$cacheKey];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Determine the appropriate model to use
|
|
113
|
+
$model = $this->determineModel($formattedHistory);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Sending a POST request to the AI API
|
|
117
|
+
$response = $this->client->request('POST', $this->apiUrl, [
|
|
118
|
+
'headers' => [
|
|
119
|
+
'Authorization' => 'Bearer ' . $this->apiKey,
|
|
120
|
+
'Content-Type' => 'application/json',
|
|
121
|
+
],
|
|
122
|
+
'json' => [
|
|
123
|
+
'model' => $model,
|
|
124
|
+
'messages' => $formattedHistory,
|
|
125
|
+
'max_tokens' => 500,
|
|
126
|
+
],
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
$responseBody = $response->getBody();
|
|
130
|
+
$responseContent = json_decode((string) $responseBody, true);
|
|
131
|
+
|
|
132
|
+
// Check if response is in expected format
|
|
133
|
+
if (isset($responseContent['choices'][0]['message']['content'])) {
|
|
134
|
+
$aiMessage = $responseContent['choices'][0]['message']['content'];
|
|
135
|
+
|
|
136
|
+
// Cache the result
|
|
137
|
+
$this->cache[$cacheKey] = $aiMessage;
|
|
138
|
+
|
|
139
|
+
return $aiMessage;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new RuntimeException('Unexpected API response format.');
|
|
143
|
+
} catch (RequestException $e) {
|
|
144
|
+
throw new RuntimeException("API request failed: " . $e->getMessage(), 0, $e);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|