@@ -134,6 +134,49 @@ def change_point(self, value: FieldType) -> "ChangePoint":
134134 """
135135 return ChangePoint (self , value )
136136
137+ def completion (
138+ self , * prompt : ExpressionType , ** named_prompt : ExpressionType
139+ ) -> "Completion" :
140+ """The `COMPLETION` command allows you to send prompts and context to a Large
141+ Language Model (LLM) directly within your ES|QL queries, to perform text
142+ generation tasks.
143+
144+ :param prompt: The input text or expression used to prompt the LLM. This can
145+ be a string literal or a reference to a column containing text.
146+ :param named_prompt: The input text or expresion, given as a keyword argument.
147+ The argument name is used for the column name. If not
148+ specified, the results will be stored in a column named
149+ `completion`. If the specified column already exists, it
150+ will be overwritten with the new results.
151+
152+ Examples::
153+
154+ query1 = (
155+ ESQL.row(question="What is Elasticsearch?")
156+ .completion("question").with_("test_completion_model")
157+ .keep("question", "completion")
158+ )
159+ query2 = (
160+ ESQL.row(question="What is Elasticsearch?")
161+ .completion(answer="question").with_("test_completion_model")
162+ .keep("question", "answer")
163+ )
164+ query3 = (
165+ ESQL.from_("movies")
166+ .sort("rating DESC")
167+ .limit(10)
168+ .eval(prompt=\" \" \" CONCAT(
169+ "Summarize this movie using the following information: \\ n",
170+ "Title: ", title, "\\ n",
171+ "Synopsis: ", synopsis, "\\ n",
172+ "Actors: ", MV_CONCAT(actors, ", "), "\\ n",
173+ )\" \" \" )
174+ .completion(summary="prompt").with_("test_completion_model")
175+ .keep("title", "summary", "rating")
176+ )
177+ """
178+ return Completion (self , * prompt , ** named_prompt )
179+
137180 def dissect (self , input : FieldType , pattern : str ) -> "Dissect" :
138181 """``DISSECT`` enables you to extract structured data out of a string.
139182
@@ -306,43 +349,39 @@ def limit(self, max_number_of_rows: int) -> "Limit":
306349 """
307350 return Limit (self , max_number_of_rows )
308351
309- def lookup_join (self , lookup_index : IndexType , field : FieldType ) -> "LookupJoin" :
352+ def lookup_join (self , lookup_index : IndexType ) -> "LookupJoin" :
310353 """`LOOKUP JOIN` enables you to add data from another index, AKA a 'lookup' index,
311354 to your ES|QL query results, simplifying data enrichment and analysis workflows.
312355
313356 :param lookup_index: The name of the lookup index. This must be a specific index
314357 name - wildcards, aliases, and remote cluster references are
315358 not supported. Indices used for lookups must be configured
316359 with the lookup index mode.
317- :param field: The field to join on. This field must exist in both your current query
318- results and in the lookup index. If the field contains multi-valued
319- entries, those entries will not match anything (the added fields will
320- contain null for those rows).
321360
322361 Examples::
323362
324363 query1 = (
325364 ESQL.from_("firewall_logs")
326- .lookup_join("threat_list", "source.IP")
365+ .lookup_join("threat_list").on( "source.IP")
327366 .where("threat_level IS NOT NULL")
328367 )
329368 query2 = (
330369 ESQL.from_("system_metrics")
331- .lookup_join("host_inventory", "host.name")
332- .lookup_join("ownerships", "host.name")
370+ .lookup_join("host_inventory").on( "host.name")
371+ .lookup_join("ownerships").on( "host.name")
333372 )
334373 query3 = (
335374 ESQL.from_("app_logs")
336- .lookup_join("service_owners", "service_id")
375+ .lookup_join("service_owners").on( "service_id")
337376 )
338377 query4 = (
339378 ESQL.from_("employees")
340379 .eval(language_code="languages")
341380 .where("emp_no >= 10091 AND emp_no < 10094")
342- .lookup_join("languages_lookup", "language_code")
381+ .lookup_join("languages_lookup").on( "language_code")
343382 )
344383 """
345- return LookupJoin (self , lookup_index , field )
384+ return LookupJoin (self , lookup_index )
346385
347386 def mv_expand (self , column : FieldType ) -> "MvExpand" :
348387 """The `MV_EXPAND` processing command expands multivalued columns into one row per
@@ -635,6 +674,47 @@ def _render_internal(self) -> str:
635674 return f"CHANGE_POINT { self ._value } { key } { names } "
636675
637676
677+ class Completion (ESQLBase ):
678+ """Implementation of the ``COMPLETION`` processing command.
679+
680+ This class inherits from :class:`ESQLBase <elasticsearch.esql.esql.ESQLBase>`,
681+ to make it possible to chain all the commands that belong to an ES|QL query
682+ in a single expression.
683+ """
684+
685+ def __init__ (
686+ self , parent : ESQLBase , * prompt : ExpressionType , ** named_prompt : ExpressionType
687+ ):
688+ if len (prompt ) + len (named_prompt ) > 1 :
689+ raise ValueError (
690+ "this method requires either one positional or one keyword argument only"
691+ )
692+ super ().__init__ (parent )
693+ self ._prompt = prompt
694+ self ._named_prompt = named_prompt
695+ self ._inference_id : Optional [str ] = None
696+
697+ def with_ (self , inference_id : str ) -> "Completion" :
698+ """Continuation of the `COMPLETION` command.
699+
700+ :param inference_id: The ID of the inference endpoint to use for the task. The
701+ inference endpoint must be configured with the completion
702+ task type.
703+ """
704+ self ._inference_id = inference_id
705+ return self
706+
707+ def _render_internal (self ) -> str :
708+ if self ._inference_id is None :
709+ raise ValueError ("The completion command requires an inference ID" )
710+ if self ._named_prompt :
711+ column = list (self ._named_prompt .keys ())[0 ]
712+ prompt = list (self ._named_prompt .values ())[0 ]
713+ return f"COMPLETION { column } = { prompt } WITH { self ._inference_id } "
714+ else :
715+ return f"COMPLETION { self ._prompt [0 ]} WITH { self ._inference_id } "
716+
717+
638718class Dissect (ESQLBase ):
639719 """Implementation of the ``DISSECT`` processing command.
640720
@@ -861,12 +941,25 @@ class LookupJoin(ESQLBase):
861941 in a single expression.
862942 """
863943
864- def __init__ (self , parent : ESQLBase , lookup_index : IndexType , field : FieldType ):
944+ def __init__ (self , parent : ESQLBase , lookup_index : IndexType ):
865945 super ().__init__ (parent )
866946 self ._lookup_index = lookup_index
947+ self ._field : Optional [FieldType ] = None
948+
949+ def on (self , field : FieldType ) -> "LookupJoin" :
950+ """Continuation of the `LOOKUP_JOIN` command.
951+
952+ :param field: The field to join on. This field must exist in both your current query
953+ results and in the lookup index. If the field contains multi-valued
954+ entries, those entries will not match anything (the added fields will
955+ contain null for those rows).
956+ """
867957 self ._field = field
958+ return self
868959
869960 def _render_internal (self ) -> str :
961+ if self ._field is None :
962+ raise ValueError ("Joins require a field to join on." )
870963 index = (
871964 self ._lookup_index
872965 if isinstance (self ._lookup_index , str )
0 commit comments