mlrun 1.10.0rc21__py3-none-any.whl → 1.10.0rc23__py3-none-any.whl

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.

Potentially problematic release.


This version of mlrun might be problematic. Click here for more details.

@@ -27,6 +27,7 @@ import mlrun
27
27
  import mlrun.common.constants as mlrun_constants
28
28
  import mlrun.common.helpers
29
29
  import mlrun.common.schemas.model_monitoring.constants as mm_constants
30
+ import mlrun.common.types
30
31
  import mlrun.datastore.datastore_profile as ds_profile
31
32
  import mlrun.errors
32
33
  import mlrun.model_monitoring.api as mm_api
@@ -39,6 +40,12 @@ from mlrun.serving.utils import MonitoringApplicationToDict
39
40
  from mlrun.utils import logger
40
41
 
41
42
 
43
+ class ExistingDataHandling(mlrun.common.types.StrEnum):
44
+ fail_on_overlap = "fail_on_overlap"
45
+ skip_overlap = "skip_overlap"
46
+ delete_all = "delete_all"
47
+
48
+
42
49
  def _serialize_context_and_result(
43
50
  *,
44
51
  context: mm_context.MonitoringApplicationContext,
@@ -288,7 +295,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
288
295
  end: Optional[str] = None,
289
296
  base_period: Optional[int] = None,
290
297
  write_output: bool = False,
291
- fail_on_overlap: bool = True,
298
+ existing_data_handling: ExistingDataHandling = ExistingDataHandling.fail_on_overlap,
292
299
  stream_profile: Optional[ds_profile.DatastoreProfile] = None,
293
300
  ):
294
301
  """
@@ -350,6 +357,24 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
350
357
  resolved_endpoints = self._handle_endpoints_type_evaluate(
351
358
  project=project, endpoints=endpoints
352
359
  )
360
+ if (
361
+ write_output
362
+ and existing_data_handling == ExistingDataHandling.delete_all
363
+ ):
364
+ endpoint_ids = [
365
+ endpoint_id for _, endpoint_id in resolved_endpoints
366
+ ]
367
+ context.logger.info(
368
+ "Deleting all the application data before running the application",
369
+ application_name=application_name,
370
+ endpoint_ids=endpoint_ids,
371
+ )
372
+ self._delete_application_data(
373
+ project_name=project.name,
374
+ application_name=application_name,
375
+ endpoint_ids=endpoint_ids,
376
+ application_schedules=application_schedules,
377
+ )
353
378
  for endpoint_name, endpoint_id in resolved_endpoints:
354
379
  for window_start, window_end in self._window_generator(
355
380
  start=start,
@@ -358,7 +383,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
358
383
  application_schedules=application_schedules,
359
384
  endpoint_id=endpoint_id,
360
385
  application_name=application_name,
361
- fail_on_overlap=fail_on_overlap,
386
+ existing_data_handling=existing_data_handling,
362
387
  ):
363
388
  result = call_do_tracking(
364
389
  event={
@@ -481,7 +506,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
481
506
  end_dt: datetime,
482
507
  base_period: Optional[int],
483
508
  application_name: str,
484
- fail_on_overlap: bool,
509
+ existing_data_handling: ExistingDataHandling,
485
510
  ) -> datetime:
486
511
  """Make sure that the (app, endpoint) pair doesn't write output before the last analyzed window"""
487
512
  if application_schedules:
@@ -490,7 +515,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
490
515
  )
491
516
  if last_analyzed:
492
517
  if start_dt < last_analyzed:
493
- if not fail_on_overlap:
518
+ if existing_data_handling == ExistingDataHandling.skip_overlap:
494
519
  if last_analyzed < end_dt and base_period is None:
495
520
  logger.warn(
496
521
  "Setting the start time to last_analyzed since the original start time precedes "
@@ -525,6 +550,25 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
525
550
  )
526
551
  return start_dt
527
552
 
553
+ @staticmethod
554
+ def _delete_application_data(
555
+ project_name: str,
556
+ application_name: str,
557
+ endpoint_ids: list[str],
558
+ application_schedules: Optional[
559
+ mm_schedules.ModelMonitoringSchedulesFileApplication
560
+ ],
561
+ ) -> None:
562
+ mlrun.get_run_db().delete_model_monitoring_metrics(
563
+ project=project_name,
564
+ application_name=application_name,
565
+ endpoint_ids=endpoint_ids,
566
+ )
567
+ if application_schedules:
568
+ application_schedules.delete_endpoints_last_analyzed(
569
+ endpoint_uids=endpoint_ids
570
+ )
571
+
528
572
  @classmethod
529
573
  def _window_generator(
530
574
  cls,
@@ -537,7 +581,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
537
581
  ],
538
582
  endpoint_id: str,
539
583
  application_name: str,
540
- fail_on_overlap: bool,
584
+ existing_data_handling: ExistingDataHandling,
541
585
  ) -> Iterator[tuple[Optional[datetime], Optional[datetime]]]:
542
586
  if start is None or end is None:
543
587
  # A single window based on the `sample_data` input - see `_handler`.
@@ -547,15 +591,16 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
547
591
  start_dt = datetime.fromisoformat(start)
548
592
  end_dt = datetime.fromisoformat(end)
549
593
 
550
- start_dt = cls._validate_monotonically_increasing_data(
551
- application_schedules=application_schedules,
552
- endpoint_id=endpoint_id,
553
- start_dt=start_dt,
554
- end_dt=end_dt,
555
- base_period=base_period,
556
- application_name=application_name,
557
- fail_on_overlap=fail_on_overlap,
558
- )
594
+ if existing_data_handling != ExistingDataHandling.delete_all:
595
+ start_dt = cls._validate_monotonically_increasing_data(
596
+ application_schedules=application_schedules,
597
+ endpoint_id=endpoint_id,
598
+ start_dt=start_dt,
599
+ end_dt=end_dt,
600
+ base_period=base_period,
601
+ application_name=application_name,
602
+ existing_data_handling=existing_data_handling,
603
+ )
559
604
 
560
605
  if base_period is None:
561
606
  yield start_dt, end_dt
@@ -702,7 +747,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
702
747
  * ``end``, ``datetime``
703
748
  * ``base_period``, ``int``
704
749
  * ``write_output``, ``bool``
705
- * ``fail_on_overlap``, ``bool``
750
+ * ``existing_data_handling``, ``str``
706
751
 
707
752
  For Git sources, add the source archive to the returned job and change the handler:
708
753
 
@@ -788,7 +833,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
788
833
  end: Optional[datetime] = None,
789
834
  base_period: Optional[int] = None,
790
835
  write_output: bool = False,
791
- fail_on_overlap: bool = True,
836
+ existing_data_handling: ExistingDataHandling = ExistingDataHandling.fail_on_overlap,
792
837
  stream_profile: Optional[ds_profile.DatastoreProfile] = None,
793
838
  ) -> "mlrun.RunObject":
794
839
  """
@@ -856,11 +901,18 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
856
901
  :param write_output: Whether to write the results and metrics to the time-series DB. Can be ``True`` only
857
902
  if ``endpoints`` are passed.
858
903
  Note: the model monitoring infrastructure must be up for the writing to work.
859
- :param fail_on_overlap: Relevant only when ``write_output=True``. When ``True``, and the
860
- requested ``start`` time precedes the ``end`` time of a previous run that also
861
- wrote to the database - an error is raised.
862
- If ``False``, when the previously described situation occurs, the relevant time
863
- window is cut so that it starts at the earliest possible time after ``start``.
904
+ :param existing_data_handling:
905
+ How to handle the existing application data for the model endpoints when writing the
906
+ new data. Relevant only when ``write_output=True``. The default is
907
+ ``"fail_on_overlap"``. The options are:
908
+
909
+ - ``"fail_on_overlap"``: when the requested ``start`` time precedes the
910
+ ``end`` time of a previous run that also wrote to the database - an error is raised.
911
+ - ``"skip_overlap"``: when the previously described situation occurs, the relevant
912
+ time window is cut so that it starts at the earliest possible time after ``start``.
913
+ - ``"delete_all"``: delete all the data that was written by the application to the
914
+ model endpoints, regardless of the time window, and write the new data.
915
+
864
916
  :param stream_profile: The stream datastore profile. It should be provided only when running locally and
865
917
  writing the outputs to the database (i.e., when both ``run_local`` and
866
918
  ``write_output`` are set to ``True``).
@@ -899,18 +951,6 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
899
951
  )
900
952
  params["end"] = end.isoformat() if isinstance(end, datetime) else end
901
953
  params["base_period"] = base_period
902
- params["write_output"] = write_output
903
- params["fail_on_overlap"] = fail_on_overlap
904
- if stream_profile:
905
- if not run_local:
906
- raise mlrun.errors.MLRunValueError(
907
- "Passing a `stream_profile` is relevant only when running locally"
908
- )
909
- if not write_output:
910
- raise mlrun.errors.MLRunValueError(
911
- "Passing a `stream_profile` is relevant only when writing the outputs"
912
- )
913
- params["stream_profile"] = stream_profile
914
954
  elif start or end or base_period:
915
955
  raise mlrun.errors.MLRunValueError(
916
956
  "Custom `start` and `end` times or base_period are supported only with endpoints data"
@@ -920,6 +960,19 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
920
960
  "Writing the application output or passing `stream_profile` are supported only with endpoints data"
921
961
  )
922
962
 
963
+ params["write_output"] = write_output
964
+ params["existing_data_handling"] = existing_data_handling
965
+ if stream_profile:
966
+ if not run_local:
967
+ raise mlrun.errors.MLRunValueError(
968
+ "Passing a `stream_profile` is relevant only when running locally"
969
+ )
970
+ if not write_output:
971
+ raise mlrun.errors.MLRunValueError(
972
+ "Passing a `stream_profile` is relevant only when writing the outputs"
973
+ )
974
+ params["stream_profile"] = stream_profile
975
+
923
976
  inputs: dict[str, str] = {}
924
977
  for data, identifier in [
925
978
  (sample_data, "sample_data"),
@@ -859,7 +859,7 @@ class MonitoringApplicationController:
859
859
  for endpoint in endpoints:
860
860
  last_request = last_request_dict.get(endpoint.metadata.uid, None)
861
861
  if isinstance(last_request, float):
862
- last_request = pd.to_datetime(last_request, unit="s", utc=True)
862
+ last_request = pd.to_datetime(last_request, unit="ms", utc=True)
863
863
  endpoint.status.last_request = (
864
864
  last_request or endpoint.status.last_request
865
865
  )
@@ -170,6 +170,16 @@ class ModelMonitoringSchedulesFileEndpoint(ModelMonitoringSchedulesFileBase):
170
170
  self._check_open_schedules()
171
171
  self._schedules[application] = float(timestamp)
172
172
 
173
+ def delete_application_time(self, application: str) -> None:
174
+ self._check_open_schedules()
175
+ if application in self._schedules:
176
+ logger.debug(
177
+ "Deleting application time from schedules",
178
+ application=application,
179
+ endpoint_id=self._endpoint_id,
180
+ )
181
+ del self._schedules[application]
182
+
173
183
  def get_application_list(self) -> set[str]:
174
184
  self._check_open_schedules()
175
185
  return set(self._schedules.keys())
@@ -275,6 +285,17 @@ class ModelMonitoringSchedulesFileApplication(ModelMonitoringSchedulesFileBase):
275
285
  timezone.utc
276
286
  ).isoformat()
277
287
 
288
+ def delete_endpoints_last_analyzed(self, endpoint_uids: list[str]) -> None:
289
+ self._check_open_schedules()
290
+ for endpoint_uid in endpoint_uids:
291
+ if endpoint_uid in self._schedules:
292
+ logger.debug(
293
+ "Deleting endpoint last analyzed from schedules",
294
+ endpoint_uid=endpoint_uid,
295
+ application=self._application,
296
+ )
297
+ del self._schedules[endpoint_uid]
298
+
278
299
 
279
300
  def _delete_folder(folder: str) -> None:
280
301
  fs = mlrun.datastore.store_manager.object(folder).store.filesystem
@@ -96,14 +96,23 @@ class TSDBConnector(ABC):
96
96
  """
97
97
 
98
98
  @abstractmethod
99
- def delete_tsdb_records(
100
- self,
101
- endpoint_ids: list[str],
102
- ) -> None:
99
+ def delete_tsdb_records(self, endpoint_ids: list[str]) -> None:
103
100
  """
104
101
  Delete model endpoint records from the TSDB connector.
102
+
105
103
  :param endpoint_ids: List of model endpoint unique identifiers.
106
- :param delete_timeout: The timeout in seconds to wait for the deletion to complete.
104
+ """
105
+ pass
106
+
107
+ @abstractmethod
108
+ def delete_application_records(
109
+ self, application_name: str, endpoint_ids: Optional[list[str]] = None
110
+ ) -> None:
111
+ """
112
+ Delete application records from the TSDB for the given model endpoints or all if ``None``.
113
+
114
+ :param application_name: The name of the application to delete records for.
115
+ :param endpoint_ids: List of model endpoint unique identifiers.
107
116
  """
108
117
  pass
109
118
 
@@ -122,10 +122,7 @@ class TDEngineSchema:
122
122
  )
123
123
  return f"DELETE FROM {self.database}.{subtable} WHERE {values};"
124
124
 
125
- def drop_subtable_query(
126
- self,
127
- subtable: str,
128
- ) -> str:
125
+ def drop_subtable_query(self, subtable: str) -> str:
129
126
  return f"DROP TABLE if EXISTS {self.database}.`{subtable}`;"
130
127
 
131
128
  def drop_supertable_query(self) -> str:
@@ -145,8 +142,10 @@ class TDEngineSchema:
145
142
  values = f" {operator} ".join(
146
143
  f"{filter_tag} LIKE '{val}'" for val in filter_values
147
144
  )
145
+ return self._get_tables_query_by_condition(values)
148
146
 
149
- return f"SELECT DISTINCT tbname FROM {self.database}.{self.super_table} WHERE {values};"
147
+ def _get_tables_query_by_condition(self, condition: str) -> str:
148
+ return f"SELECT DISTINCT TBNAME FROM {self.database}.{self.super_table} WHERE {condition};"
150
149
 
151
150
  @staticmethod
152
151
  def _get_records_query(
@@ -22,7 +22,6 @@ import taosws
22
22
  import mlrun.common.schemas.model_monitoring as mm_schemas
23
23
  import mlrun.common.types
24
24
  import mlrun.model_monitoring.db.tsdb.tdengine.schemas as tdengine_schemas
25
- import mlrun.model_monitoring.db.tsdb.tdengine.stream_graph_steps
26
25
  from mlrun.datastore.datastore_profile import DatastoreProfile
27
26
  from mlrun.model_monitoring.db import TSDBConnector
28
27
  from mlrun.model_monitoring.db.tsdb.tdengine.tdengine_connection import (
@@ -205,7 +204,7 @@ class TDEngineConnector(TSDBConnector):
205
204
  @staticmethod
206
205
  def _generate_filter_query(
207
206
  filter_column: str, filter_values: Union[str, list[Union[str, int]]]
208
- ) -> Optional[str]:
207
+ ) -> str:
209
208
  """
210
209
  Generate a filter query for TDEngine based on the provided column and values.
211
210
 
@@ -213,15 +212,14 @@ class TDEngineConnector(TSDBConnector):
213
212
  :param filter_values: A single value or a list of values to filter by.
214
213
 
215
214
  :return: A string representing the filter query.
216
- :raise: MLRunInvalidArgumentError if the filter values are not of type string or list.
215
+ :raise: ``MLRunValueError`` if the filter values are not of type string or list.
217
216
  """
218
-
219
217
  if isinstance(filter_values, str):
220
218
  return f"{filter_column}='{filter_values}'"
221
219
  elif isinstance(filter_values, list):
222
220
  return f"{filter_column} IN ({', '.join(repr(v) for v in filter_values)}) "
223
221
  else:
224
- raise mlrun.errors.MLRunInvalidArgumentError(
222
+ raise mlrun.errors.MLRunValueError(
225
223
  f"Invalid filter values {filter_values}: must be a string or a list, "
226
224
  f"got {type(filter_values).__name__}; filter values: {filter_values}"
227
225
  )
@@ -311,10 +309,7 @@ class TDEngineConnector(TSDBConnector):
311
309
  flush_after_seconds=tsdb_batching_timeout_secs,
312
310
  )
313
311
 
314
- def delete_tsdb_records(
315
- self,
316
- endpoint_ids: list[str],
317
- ):
312
+ def delete_tsdb_records(self, endpoint_ids: list[str]) -> None:
318
313
  """
319
314
  To delete subtables within TDEngine, we first query the subtables names with the provided endpoint_ids.
320
315
  Then, we drop each subtable.
@@ -332,9 +327,7 @@ class TDEngineConnector(TSDBConnector):
332
327
  get_subtable_query = self.tables[table]._get_subtables_query_by_tag(
333
328
  filter_tag="endpoint_id", filter_values=endpoint_ids
334
329
  )
335
- subtables_result = self.connection.run(
336
- query=get_subtable_query,
337
- )
330
+ subtables_result = self.connection.run(query=get_subtable_query)
338
331
  subtables.extend([subtable[0] for subtable in subtables_result.data])
339
332
  except Exception as e:
340
333
  logger.warning(
@@ -346,15 +339,13 @@ class TDEngineConnector(TSDBConnector):
346
339
  )
347
340
 
348
341
  # Prepare the drop statements
349
- drop_statements = []
350
- for subtable in subtables:
351
- drop_statements.append(
352
- self.tables[table].drop_subtable_query(subtable=subtable)
353
- )
342
+ drop_statements = [
343
+ self.tables[table].drop_subtable_query(subtable=subtable)
344
+ for subtable in subtables
345
+ ]
354
346
  try:
355
- self.connection.run(
356
- statements=drop_statements,
357
- )
347
+ logger.debug("Dropping subtables", drop_statements=drop_statements)
348
+ self.connection.run(statements=drop_statements)
358
349
  except Exception as e:
359
350
  logger.warning(
360
351
  "Failed to delete model endpoint resources. You may need to delete them manually. "
@@ -369,6 +360,48 @@ class TDEngineConnector(TSDBConnector):
369
360
  number_of_endpoints_to_delete=len(endpoint_ids),
370
361
  )
371
362
 
363
+ def delete_application_records(
364
+ self, application_name: str, endpoint_ids: Optional[list[str]] = None
365
+ ) -> None:
366
+ """
367
+ Delete application records from the TSDB for the given model endpoints or all if ``endpoint_ids`` is ``None``.
368
+ """
369
+ logger.debug(
370
+ "Deleting application records",
371
+ project=self.project,
372
+ application_name=application_name,
373
+ endpoint_ids=endpoint_ids,
374
+ )
375
+ tables = [
376
+ self.tables[mm_schemas.TDEngineSuperTables.APP_RESULTS],
377
+ self.tables[mm_schemas.TDEngineSuperTables.METRICS],
378
+ ]
379
+
380
+ filter_query = self._generate_filter_query(
381
+ filter_column=mm_schemas.ApplicationEvent.APPLICATION_NAME,
382
+ filter_values=application_name,
383
+ )
384
+ if endpoint_ids:
385
+ endpoint_ids_filter = self._generate_filter_query(
386
+ filter_column=mm_schemas.EventFieldType.ENDPOINT_ID,
387
+ filter_values=endpoint_ids,
388
+ )
389
+ filter_query += f" AND {endpoint_ids_filter}"
390
+
391
+ drop_statements: list[str] = []
392
+ for table in tables:
393
+ get_subtable_query = table._get_tables_query_by_condition(filter_query)
394
+ subtables_result = self.connection.run(query=get_subtable_query)
395
+ drop_statements.extend(
396
+ [
397
+ table.drop_subtable_query(subtable=subtable[0])
398
+ for subtable in subtables_result.data
399
+ ]
400
+ )
401
+
402
+ logger.debug("Dropping application records", drop_statements=drop_statements)
403
+ self.connection.run(statements=drop_statements)
404
+
372
405
  def delete_tsdb_resources(self):
373
406
  """
374
407
  Delete all project resources in the TSDB connector, such as model endpoints data and drift results.
@@ -492,7 +492,8 @@ class V3IOTSDBConnector(TSDBConnector):
492
492
  # Split the endpoint ids into chunks to avoid exceeding the v3io-engine filter-expression limit
493
493
  for i in range(0, len(endpoint_ids), V3IO_FRAMESD_MEPS_LIMIT):
494
494
  endpoint_id_chunk = endpoint_ids[i : i + V3IO_FRAMESD_MEPS_LIMIT]
495
- filter_query = f"endpoint_id IN({str(endpoint_id_chunk)[1:-1]}) "
495
+ endpoints_list = "', '".join(endpoint_id_chunk)
496
+ filter_query = f"endpoint_id IN('{endpoints_list}')"
496
497
  for table in tables:
497
498
  try:
498
499
  self.frames_client.delete(
@@ -532,6 +533,43 @@ class V3IOTSDBConnector(TSDBConnector):
532
533
  project=self.project,
533
534
  )
534
535
 
536
+ def delete_application_records(
537
+ self, application_name: str, endpoint_ids: Optional[list[str]] = None
538
+ ) -> None:
539
+ """
540
+ Delete application records from the TSDB for the given model endpoints or all if ``endpoint_ids`` is ``None``.
541
+ """
542
+ base_filter_query = f"application_name=='{application_name}'"
543
+
544
+ filter_queries: list[str] = []
545
+ if endpoint_ids:
546
+ for i in range(0, len(endpoint_ids), V3IO_FRAMESD_MEPS_LIMIT):
547
+ endpoint_id_chunk = endpoint_ids[i : i + V3IO_FRAMESD_MEPS_LIMIT]
548
+ endpoints_list = "', '".join(endpoint_id_chunk)
549
+ filter_queries.append(
550
+ f"{base_filter_query} AND endpoint_id IN ('{endpoints_list}')"
551
+ )
552
+ else:
553
+ filter_queries = [base_filter_query]
554
+
555
+ for table in [
556
+ self.tables[mm_schemas.V3IOTSDBTables.APP_RESULTS],
557
+ self.tables[mm_schemas.V3IOTSDBTables.METRICS],
558
+ ]:
559
+ logger.debug(
560
+ "Deleting application records from TSDB",
561
+ table=table,
562
+ filter_queries=filter_queries,
563
+ project=self.project,
564
+ )
565
+ for filter_query in filter_queries:
566
+ self.frames_client.delete(
567
+ backend=_TSDB_BE,
568
+ table=table,
569
+ filter=filter_query,
570
+ start="0",
571
+ )
572
+
535
573
  def get_model_endpoint_real_time_metrics(
536
574
  self, endpoint_id: str, metrics: list[str], start: str, end: str
537
575
  ) -> dict[str, list[tuple[str, float]]]:
mlrun/projects/project.py CHANGED
@@ -1908,13 +1908,51 @@ class MlrunProject(ModelObj):
1908
1908
 
1909
1909
  Examples::
1910
1910
 
1911
+ # Log directly with an inline prompt template
1912
+ project.log_llm_prompt(
1913
+ key="customer_support_prompt",
1914
+ prompt_template=[
1915
+ {
1916
+ "role": "system",
1917
+ "content": "You are a helpful customer support assistant.",
1918
+ },
1919
+ {
1920
+ "role": "user",
1921
+ "content": "The customer reports: {issue_description}",
1922
+ },
1923
+ ],
1924
+ prompt_legend={
1925
+ "issue_description": {
1926
+ "field": "user_issue",
1927
+ "description": "Detailed description of the customer's issue",
1928
+ },
1929
+ "solution": {
1930
+ "field": "proposed_solution",
1931
+ "description": "Suggested fix for the customer's issue",
1932
+ },
1933
+ },
1934
+ model_artifact=model,
1935
+ model_configuration={"temperature": 0.5, "max_tokens": 200},
1936
+ description="Prompt for handling customer support queries",
1937
+ tag="support-v1",
1938
+ labels={"domain": "support"},
1939
+ )
1940
+
1911
1941
  # Log a prompt from file
1912
1942
  project.log_llm_prompt(
1913
- key="qa-prompt",
1914
- prompt_path="prompts/qa_template.txt",
1915
- prompt_legend={"question": "user_question"},
1943
+ key="qa_prompt",
1944
+ prompt_path="prompts/template.json",
1945
+ prompt_legend={
1946
+ "question": {
1947
+ "field": "user_question",
1948
+ "description": "The actual question asked by the user",
1949
+ }
1950
+ },
1916
1951
  model_artifact=model,
1952
+ model_configuration={"temperature": 0.7, "max_tokens": 256},
1953
+ description="Q&A prompt template with user-provided question",
1917
1954
  tag="v2",
1955
+ labels={"task": "qa", "stage": "experiment"},
1918
1956
  )
1919
1957
 
1920
1958
  :param key: Unique key for the prompt artifact.
@@ -1923,7 +1961,10 @@ class MlrunProject(ModelObj):
1923
1961
  "role": "user", "content": "I need your help with {profession}"]. only "role" and "content" keys allow in any
1924
1962
  str format (upper/lower case), keys will be modified to lower case.
1925
1963
  Cannot be used with `prompt_path`.
1926
- :param prompt_path: Path to a file containing the prompt. Mutually exclusive with `prompt_string`.
1964
+ :param prompt_path: Path to a JSON file containing the prompt template.
1965
+ Cannot be used together with `prompt_template`.
1966
+ The file should define a list of dictionaries in the same format
1967
+ supported by `prompt_template`.
1927
1968
  :param prompt_legend: A dictionary where each key is a placeholder in the prompt (e.g., ``{user_name}``)
1928
1969
  and the value is a dictionary holding two keys, "field", "description". "field" points to the field in
1929
1970
  the event where the value of the place-holder inside the event, if None or not exist will be replaced
@@ -1932,9 +1973,11 @@ class MlrunProject(ModelObj):
1932
1973
  :param model_artifact: Reference to the parent model (either `ModelArtifact` or model URI string).
1933
1974
  :param model_configuration: Configuration dictionary for model generation parameters
1934
1975
  (e.g., temperature, max tokens).
1935
- :param description: Optional description of the prompt.
1936
- :param target_path: Optional local target path for saving prompt content.
1937
- :param artifact_path: Storage path for the logged artifact.
1976
+ :param description: Optional description of the prompt.
1977
+ :param target_path: Absolute target path (instead of using artifact_path + local_path)
1978
+ :param artifact_path: Target artifact path (when not using the default)
1979
+ To define a subpath under the default location use:
1980
+ `artifact_path=context.artifact_subpath('data')`
1938
1981
  :param tag: Version tag for the artifact (e.g., "v1", "latest").
1939
1982
  :param labels: Labels to tag the artifact for filtering and organization.
1940
1983
  :param upload: Whether to upload the artifact to a remote datastore. Defaults to True.
mlrun/serving/server.py CHANGED
@@ -17,8 +17,10 @@ __all__ = ["GraphServer", "create_graph_server", "GraphContext", "MockEvent"]
17
17
  import asyncio
18
18
  import base64
19
19
  import copy
20
+ import importlib
20
21
  import json
21
22
  import os
23
+ import pathlib
22
24
  import socket
23
25
  import traceback
24
26
  import uuid
@@ -572,19 +574,34 @@ async def async_execute_graph(
572
574
  nest_under_inputs: bool,
573
575
  ) -> list[Any]:
574
576
  spec = mlrun.utils.get_serving_spec()
575
-
576
- namespace = {}
577
+ modname = None
577
578
  code = os.getenv("MLRUN_EXEC_CODE")
578
579
  if code:
579
580
  code = base64.b64decode(code).decode("utf-8")
580
- exec(code, namespace)
581
+ with open("user_code.py", "w") as fp:
582
+ fp.write(code)
583
+ modname = "user_code"
581
584
  else:
582
585
  # TODO: find another way to get the local file path, or ensure that MLRUN_EXEC_CODE
583
586
  # gets set in local flow and not just in the remote pod
584
- source_filename = spec.get("filename", None)
585
- if source_filename:
586
- with open(source_filename) as f:
587
- exec(f.read(), namespace)
587
+ source_file_path = spec.get("filename", None)
588
+ if source_file_path:
589
+ source_file_path_object = pathlib.Path(source_file_path).resolve()
590
+ current_dir_path_object = pathlib.Path(".").resolve()
591
+ if not source_file_path_object.is_relative_to(current_dir_path_object):
592
+ raise mlrun.errors.MLRunRuntimeError(
593
+ f"Source file path '{source_file_path}' is not under the current working directory "
594
+ f"(which is required when running with local=True)"
595
+ )
596
+ relative_path_to_source_file = source_file_path_object.relative_to(
597
+ current_dir_path_object
598
+ )
599
+ modname = ".".join(relative_path_to_source_file.with_suffix("").parts)
600
+
601
+ namespace = {}
602
+ if modname:
603
+ mod = importlib.import_module(modname)
604
+ namespace = mod.__dict__
588
605
 
589
606
  server = GraphServer.from_dict(spec)
590
607