1212namespace Manticoresearch \Buddy \Base \Plugin \ConversationalSearch ;
1313
1414use Manticoresearch \Buddy \Core \Error \QueryParseError ;
15+ use Manticoresearch \Buddy \Core \ManticoreSearch \Endpoint as ManticoreEndpoint ;
1516use Manticoresearch \Buddy \Core \Network \Request ;
1617use Manticoresearch \Buddy \Core \Plugin \BasePayload ;
1718
@@ -24,6 +25,9 @@ final class Payload extends BasePayload {
2425 public const string ACTION_DESCRIBE_MODEL = 'describe_model ' ;
2526 public const string ACTION_DROP_MODEL = 'drop_model ' ;
2627 public const string ACTION_CONVERSATION = 'conversation ' ;
28+ private const array CONVERSATION_JSON_FIELDS = [
29+ 'query ' , 'table ' , 'model_name ' , 'conversation_uuid ' , 'vector_field ' , 'fields ' ,
30+ ];
2731
2832 /** @var string */
2933 public string $ action ;
@@ -42,6 +46,10 @@ final class Payload extends BasePayload {
4246 * @return bool
4347 */
4448 public static function hasMatch (Request $ request ): bool {
49+ if (self ::matchesJson ($ request )) {
50+ return true ;
51+ }
52+
4553 // Check SQL patterns first
4654 if (self ::matchesSQL ($ request )) {
4755 return true ;
@@ -50,6 +58,22 @@ public static function hasMatch(Request $request): bool {
5058 return false ;
5159 }
5260
61+ /**
62+ * Check if HTTP JSON /search request contains chat payload
63+ *
64+ * @param Request $request
65+ *
66+ * @return bool
67+ */
68+ private static function matchesJson (Request $ request ): bool {
69+ if ($ request ->endpointBundle !== ManticoreEndpoint::Search) {
70+ return false ;
71+ }
72+
73+ $ payload = simdjson_decode ($ request ->payload , true );
74+ return is_array ($ payload ) && isset ($ payload ['chat ' ]) && is_array ($ payload ['chat ' ]);
75+ }
76+
5377 /**
5478 * Check if SQL query matches chat patterns
5579 *
@@ -94,15 +118,125 @@ private static function matchesSQL(Request $request): bool {
94118 * @throws QueryParseError
95119 */
96120 public static function fromRequest (Request $ request ): static {
97- $ payload = new static ();
98- $ payload ->query = $ request ->payload ;
121+ return match ($ request ->endpointBundle ) {
122+ ManticoreEndpoint::Search => static ::fromJsonRequest ($ request ),
123+ default => static ::fromSqlRequest ($ request ),
124+ };
125+ }
126+
127+ /**
128+ * @param Request $request
129+ *
130+ * @return static
131+ * @throws QueryParseError
132+ */
133+ protected static function fromJsonRequest (Request $ request ): static {
134+ $ self = new static ();
135+ $ self ->query = $ request ->payload ;
136+ $ payload = $ self ->decodeJsonBody ($ request );
137+ if (!isset ($ payload ['chat ' ]) || !is_array ($ payload ['chat ' ])) {
138+ throw QueryParseError::create ('HTTP JSON body must contain chat object ' );
139+ }
140+
141+ $ self ->parseJsonConversation ($ payload ['chat ' ]);
142+ return $ self ;
143+ }
144+
145+ /**
146+ * @param Request $request
147+ *
148+ * @return static
149+ * @throws QueryParseError
150+ */
151+ protected static function fromSqlRequest (Request $ request ): static {
152+ $ self = new static ();
153+ $ self ->query = $ request ->payload ;
154+ $ self ->parseSQLRequest ($ request );
155+ return $ self ;
156+ }
157+
158+ /**
159+ * @param Request $request
160+ *
161+ * @return array<string, mixed>
162+ * @throws QueryParseError
163+ */
164+ private function decodeJsonBody (Request $ request ): array {
165+ if (trim ($ request ->payload ) === '' ) {
166+ throw QueryParseError::create ('HTTP JSON body is required ' );
167+ }
99168
100- // Parse SQL request only (HTTP API not supported)
101- $ payload ->parseSQLRequest ($ request );
169+ $ payload = simdjson_decode ($ request ->payload , true );
170+ if (!is_array ($ payload )) {
171+ throw QueryParseError::create ('HTTP JSON body must be an object ' );
172+ }
102173
103174 return $ payload ;
104175 }
105176
177+ /**
178+ * @param array<string, mixed> $payload
179+ *
180+ * @return void
181+ * @throws QueryParseError
182+ */
183+ private function parseJsonConversation (array $ payload ): void {
184+ $ this ->action = self ::ACTION_CONVERSATION ;
185+ $ hasVectorField = false ;
186+ $ hasFields = false ;
187+
188+ foreach ($ payload as $ field => $ value ) {
189+ $ this ->parseJsonConversationField ($ field , $ value , $ hasVectorField , $ hasFields );
190+ }
191+
192+ if ($ hasVectorField && $ hasFields ) {
193+ throw QueryParseError::create ('Use either vector_field or fields, not both ' );
194+ }
195+
196+ $ this ->validateRequiredJsonConversationParams ();
197+ }
198+
199+ /**
200+ * @throws QueryParseError
201+ */
202+ private function parseJsonConversationField (
203+ string $ field ,
204+ mixed $ value ,
205+ bool &$ hasVectorField ,
206+ bool &$ hasFields
207+ ): void {
208+ if (!in_array ($ field , self ::CONVERSATION_JSON_FIELDS , true )) {
209+ throw QueryParseError::create ("Unknown chat JSON field: $ field " );
210+ }
211+
212+ if (!is_string ($ value )) {
213+ throw QueryParseError::create ("$ field must be a string " );
214+ }
215+
216+ if ($ field === 'vector_field ' ) {
217+ $ hasVectorField = true ;
218+ $ this ->params ['fields ' ] = $ value ;
219+ return ;
220+ }
221+
222+ if ($ field === 'fields ' ) {
223+ $ hasFields = true ;
224+ }
225+
226+ $ this ->params [$ field ] = $ value ;
227+ }
228+
229+ /**
230+ * @throws QueryParseError
231+ */
232+ private function validateRequiredJsonConversationParams (): void {
233+ foreach (['query ' , 'table ' , 'model_name ' ] as $ field ) {
234+ if (!isset ($ this ->params [$ field ]) || trim ($ this ->params [$ field ]) === '' ) {
235+ throw QueryParseError::create ("$ field must be a non-empty string " );
236+ }
237+ }
238+ }
239+
106240
107241 /**
108242 * Parse SQL request
0 commit comments