flowcept 0.8.10__py3-none-any.whl → 0.8.12__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.
Files changed (64) hide show
  1. flowcept/__init__.py +7 -4
  2. flowcept/agents/__init__.py +5 -0
  3. flowcept/agents/agent_client.py +58 -0
  4. flowcept/agents/agents_utils.py +181 -0
  5. flowcept/agents/dynamic_schema_tracker.py +191 -0
  6. flowcept/agents/flowcept_agent.py +30 -0
  7. flowcept/agents/flowcept_ctx_manager.py +175 -0
  8. flowcept/agents/gui/__init__.py +5 -0
  9. flowcept/agents/gui/agent_gui.py +76 -0
  10. flowcept/agents/gui/gui_utils.py +239 -0
  11. flowcept/agents/llms/__init__.py +1 -0
  12. flowcept/agents/llms/claude_gcp.py +139 -0
  13. flowcept/agents/llms/gemini25.py +119 -0
  14. flowcept/agents/prompts/__init__.py +1 -0
  15. flowcept/agents/prompts/general_prompts.py +69 -0
  16. flowcept/agents/prompts/in_memory_query_prompts.py +297 -0
  17. flowcept/agents/tools/__init__.py +1 -0
  18. flowcept/agents/tools/general_tools.py +102 -0
  19. flowcept/agents/tools/in_memory_queries/__init__.py +1 -0
  20. flowcept/agents/tools/in_memory_queries/in_memory_queries_tools.py +704 -0
  21. flowcept/agents/tools/in_memory_queries/pandas_agent_utils.py +309 -0
  22. flowcept/cli.py +459 -17
  23. flowcept/commons/daos/docdb_dao/mongodb_dao.py +47 -0
  24. flowcept/commons/daos/keyvalue_dao.py +19 -23
  25. flowcept/commons/daos/mq_dao/mq_dao_base.py +49 -38
  26. flowcept/commons/daos/mq_dao/mq_dao_kafka.py +20 -3
  27. flowcept/commons/daos/mq_dao/mq_dao_mofka.py +4 -0
  28. flowcept/commons/daos/mq_dao/mq_dao_redis.py +38 -5
  29. flowcept/commons/daos/redis_conn.py +47 -0
  30. flowcept/commons/flowcept_dataclasses/task_object.py +50 -27
  31. flowcept/commons/flowcept_dataclasses/workflow_object.py +9 -1
  32. flowcept/commons/settings_factory.py +2 -4
  33. flowcept/commons/task_data_preprocess.py +400 -0
  34. flowcept/commons/utils.py +26 -7
  35. flowcept/configs.py +48 -29
  36. flowcept/flowcept_api/flowcept_controller.py +102 -18
  37. flowcept/flowceptor/adapters/base_interceptor.py +24 -11
  38. flowcept/flowceptor/adapters/brokers/__init__.py +1 -0
  39. flowcept/flowceptor/adapters/brokers/mqtt_interceptor.py +132 -0
  40. flowcept/flowceptor/adapters/mlflow/mlflow_interceptor.py +3 -3
  41. flowcept/flowceptor/adapters/tensorboard/tensorboard_interceptor.py +3 -3
  42. flowcept/flowceptor/consumers/agent/__init__.py +1 -0
  43. flowcept/flowceptor/consumers/agent/base_agent_context_manager.py +125 -0
  44. flowcept/flowceptor/consumers/base_consumer.py +94 -0
  45. flowcept/flowceptor/consumers/consumer_utils.py +5 -4
  46. flowcept/flowceptor/consumers/document_inserter.py +135 -36
  47. flowcept/flowceptor/telemetry_capture.py +6 -3
  48. flowcept/instrumentation/flowcept_agent_task.py +294 -0
  49. flowcept/instrumentation/flowcept_decorator.py +43 -0
  50. flowcept/instrumentation/flowcept_loop.py +3 -3
  51. flowcept/instrumentation/flowcept_task.py +64 -24
  52. flowcept/instrumentation/flowcept_torch.py +5 -5
  53. flowcept/instrumentation/task_capture.py +87 -4
  54. flowcept/version.py +1 -1
  55. {flowcept-0.8.10.dist-info → flowcept-0.8.12.dist-info}/METADATA +48 -11
  56. flowcept-0.8.12.dist-info/RECORD +101 -0
  57. resources/sample_settings.yaml +46 -14
  58. flowcept/flowceptor/adapters/zambeze/__init__.py +0 -1
  59. flowcept/flowceptor/adapters/zambeze/zambeze_dataclasses.py +0 -41
  60. flowcept/flowceptor/adapters/zambeze/zambeze_interceptor.py +0 -102
  61. flowcept-0.8.10.dist-info/RECORD +0 -75
  62. {flowcept-0.8.10.dist-info → flowcept-0.8.12.dist-info}/WHEEL +0 -0
  63. {flowcept-0.8.10.dist-info → flowcept-0.8.12.dist-info}/entry_points.txt +0 -0
  64. {flowcept-0.8.10.dist-info → flowcept-0.8.12.dist-info}/licenses/LICENSE +0 -0
flowcept/cli.py CHANGED
@@ -14,6 +14,9 @@ Supports:
14
14
  - `flowcept --help --command` for command-specific help
15
15
  """
16
16
 
17
+ import subprocess
18
+ import shlex
19
+ from typing import Dict, Optional
17
20
  import argparse
18
21
  import os
19
22
  import sys
@@ -21,9 +24,11 @@ import json
21
24
  import textwrap
22
25
  import inspect
23
26
  from functools import wraps
27
+ from importlib import resources
28
+ from pathlib import Path
24
29
  from typing import List
25
30
 
26
- from flowcept import Flowcept, configs
31
+ from flowcept import configs
27
32
 
28
33
 
29
34
  def no_docstring(func):
@@ -36,7 +41,7 @@ def no_docstring(func):
36
41
  return wrapper
37
42
 
38
43
 
39
- def show_config():
44
+ def show_settings():
40
45
  """
41
46
  Show Flowcept configuration.
42
47
  """
@@ -46,11 +51,154 @@ def show_config():
46
51
  }
47
52
  print(f"This is the settings path in this session: {configs.SETTINGS_PATH}")
48
53
  print(
49
- f"This is your FLOWCEPT_SETTINGS_PATH environment variable value: "
50
- f"{config_data['env_FLOWCEPT_SETTINGS_PATH']}"
54
+ f"This is your FLOWCEPT_SETTINGS_PATH environment variable value: {config_data['env_FLOWCEPT_SETTINGS_PATH']}"
51
55
  )
52
56
 
53
57
 
58
+ def init_settings(full: bool = False):
59
+ """
60
+ Create a new settings.yaml file in your home directory under ~/.flowcept.
61
+
62
+ Parameters
63
+ ----------
64
+ full : bool, optional -- Run with full to generate a complete version of the settings file.
65
+ """
66
+ settings_path_env = os.getenv("FLOWCEPT_SETTINGS_PATH", None)
67
+ if settings_path_env is not None:
68
+ print(f"FLOWCEPT_SETTINGS_PATH environment variable is set to {settings_path_env}.")
69
+ dest_path = settings_path_env
70
+ else:
71
+ dest_path = Path(os.path.join(configs._SETTINGS_DIR, "settings.yaml"))
72
+
73
+ if dest_path.exists():
74
+ overwrite = input(f"{dest_path} already exists. Overwrite? (y/N): ").strip().lower()
75
+ if overwrite != "y":
76
+ print("Operation aborted.")
77
+ return
78
+
79
+ os.makedirs(configs._SETTINGS_DIR, exist_ok=True)
80
+
81
+ if full:
82
+ print("Going to generate full settings.yaml.")
83
+ sample_settings_path = str(resources.files("resources").joinpath("sample_settings.yaml"))
84
+ with open(sample_settings_path, "rb") as src_file, open(dest_path, "wb") as dst_file:
85
+ dst_file.write(src_file.read())
86
+ print(f"Copied {sample_settings_path} to {dest_path}")
87
+ else:
88
+ from omegaconf import OmegaConf
89
+
90
+ cfg = OmegaConf.create(configs.DEFAULT_SETTINGS)
91
+ OmegaConf.save(cfg, dest_path)
92
+ print(f"Generated default settings under {dest_path}.")
93
+
94
+
95
+ def version():
96
+ """
97
+ Returns this Flowcept's installation version.
98
+ """
99
+ from flowcept.version import __version__
100
+
101
+ print(f"Flowcept {__version__}")
102
+
103
+
104
+ def stream_messages(print_messages: bool = False, messages_file_path: Optional[str] = None):
105
+ """
106
+ Listen to Flowcept's message stream and optionally echo/save messages.
107
+
108
+ Parameters.
109
+ ----------
110
+ print_messages : bool, optional
111
+ If True, print each decoded message to stdout.
112
+ messages_file_path : str, optional
113
+ If provided, append each message as JSON (one per line) to this file.
114
+ If the file already exists, a new timestamped file is created instead.
115
+ """
116
+ # Local imports to avoid changing module-level deps
117
+ from flowcept.configs import MQ_TYPE
118
+
119
+ if MQ_TYPE != "redis":
120
+ print("This is currently only available for Redis. Other MQ impls coming soon.")
121
+ return
122
+
123
+ import os
124
+ import json
125
+ from datetime import datetime
126
+ import redis
127
+ import msgpack
128
+ from flowcept.configs import MQ_HOST, MQ_PORT, MQ_CHANNEL, KVDB_URI
129
+ from flowcept.commons.daos.mq_dao.mq_dao_redis import MQDaoRedis
130
+
131
+ def _timestamped_path_if_exists(path: Optional[str]) -> Optional[str]:
132
+ if not path:
133
+ return path
134
+ if os.path.exists(path):
135
+ base, ext = os.path.splitext(path)
136
+ ts = datetime.now().strftime("%Y-%m-%d %H.%M.%S")
137
+ return f"{base} ({ts}){ext}"
138
+ return path
139
+
140
+ def _json_dumps(obj) -> str:
141
+ """JSON-dump a msgpack-decoded object; handle bytes safely."""
142
+
143
+ def _default(o):
144
+ if isinstance(o, (bytes, bytearray)):
145
+ try:
146
+ return o.decode("utf-8")
147
+ except Exception:
148
+ return o.hex()
149
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
150
+
151
+ return json.dumps(obj, ensure_ascii=False, separators=(",", ":"), default=_default)
152
+
153
+ # Prepare output file (JSONL)
154
+ out_fh = None
155
+ if messages_file_path:
156
+ out_path = _timestamped_path_if_exists(messages_file_path)
157
+ out_fh = open(out_path, "w", encoding="utf-8", buffering=1) # line-buffered
158
+
159
+ # Connect & subscribe
160
+ redis_client = redis.from_url(KVDB_URI) if KVDB_URI else redis.Redis(host=MQ_HOST, port=MQ_PORT, db=0)
161
+ pubsub = redis_client.pubsub()
162
+ pubsub.subscribe(MQ_CHANNEL)
163
+
164
+ print(f"Listening for messages on channel '{MQ_CHANNEL}'... (Ctrl+C to exit)")
165
+
166
+ try:
167
+ for message in pubsub.listen():
168
+ if not message or message.get("type") in MQDaoRedis.MESSAGE_TYPES_IGNORE:
169
+ continue
170
+
171
+ data = message.get("data")
172
+ if not isinstance(data, (bytes, bytearray)):
173
+ print(f"Skipping message with unexpected data type: {type(data)} - {data}")
174
+ continue
175
+
176
+ try:
177
+ msg_obj = msgpack.loads(data, strict_map_key=False)
178
+ msg_type = msg_obj.get("type", None)
179
+ print(f"\nReceived a message! type={msg_type}")
180
+
181
+ if print_messages:
182
+ print(_json_dumps(msg_obj))
183
+
184
+ if out_fh is not None:
185
+ out_fh.write(_json_dumps(msg_obj))
186
+ out_fh.write("\n")
187
+
188
+ except Exception as e:
189
+ print(f"Error decoding message: {e}")
190
+
191
+ except KeyboardInterrupt:
192
+ print("\nInterrupted, shutting down...")
193
+ finally:
194
+ try:
195
+ if out_fh:
196
+ out_fh.close()
197
+ pubsub.close()
198
+ except Exception:
199
+ pass
200
+
201
+
54
202
  def start_consumption_services(bundle_exec_id: str = None, check_safe_stops: bool = False, consumers: List[str] = None):
55
203
  """
56
204
  Start services that consume data from a queue or other source.
@@ -69,6 +217,8 @@ def start_consumption_services(bundle_exec_id: str = None, check_safe_stops: boo
69
217
  print(f" check_safe_stops: {check_safe_stops}")
70
218
  print(f" consumers: {consumers or []}")
71
219
 
220
+ from flowcept import Flowcept
221
+
72
222
  Flowcept.start_consumption_services(
73
223
  bundle_exec_id=bundle_exec_id,
74
224
  check_safe_stops=check_safe_stops,
@@ -112,6 +262,8 @@ def workflow_count(workflow_id: str):
112
262
  workflow_id : str
113
263
  The ID of the workflow to count tasks for.
114
264
  """
265
+ from flowcept import Flowcept
266
+
115
267
  result = {
116
268
  "workflow_id": workflow_id,
117
269
  "tasks": len(Flowcept.db.query({"workflow_id": workflow_id})),
@@ -121,28 +273,316 @@ def workflow_count(workflow_id: str):
121
273
  print(json.dumps(result, indent=2))
122
274
 
123
275
 
124
- def query(query_str: str):
276
+ def query(filter: str, project: str = None, sort: str = None, limit: int = 0):
277
+ """
278
+ Query the MongoDB task collection with an optional projection, sort, and limit.
279
+
280
+ Parameters
281
+ ----------
282
+ filter : str
283
+ A JSON string representing the MongoDB filter query.
284
+ project : str, optional
285
+ A JSON string specifying fields to include or exclude in the result (MongoDB projection).
286
+ sort : str, optional
287
+ A JSON string specifying sorting criteria (e.g., '[["started_at", -1]]').
288
+ limit : int, optional
289
+ Maximum number of documents to return. Default is 0 (no limit).
290
+
291
+ Returns
292
+ -------
293
+ List[dict]
294
+ A list of task documents matching the query.
125
295
  """
126
- Query the Document DB.
296
+ from flowcept import Flowcept
297
+
298
+ _filter, _project, _sort = None, None, None
299
+ if filter:
300
+ _filter = json.loads(filter)
301
+ if project:
302
+ _project = json.loads(project)
303
+ if sort:
304
+ _sort = list(sort)
305
+ print(
306
+ json.dumps(
307
+ Flowcept.db.query(filter=_filter, projection=_project, sort=_sort, limit=limit), indent=2, default=str
308
+ )
309
+ )
310
+
311
+
312
+ def get_task(task_id: str):
313
+ """
314
+ Query the Document DB to retrieve a task.
315
+
316
+ Parameters
317
+ ----------
318
+ task_id : str
319
+ The identifier of the task.
320
+ """
321
+ from flowcept import Flowcept
322
+
323
+ _query = {"task_id": task_id}
324
+ print(json.dumps(Flowcept.db.query(_query), indent=2, default=str))
325
+
326
+
327
+ def start_agent(): # TODO: start with gui
328
+ """Start Flowcept agent."""
329
+ from flowcept.agents.flowcept_agent import main
330
+
331
+ main()
332
+
333
+
334
+ def start_agent_gui(port: int = None):
335
+ """Start Flowcept agent GUI service.
127
336
 
128
337
  Parameters
129
338
  ----------
130
- query_str : str
131
- A JSON string representing the Mongo query.
339
+ port : int, optional
340
+ The default port is 8501. Use --port if you want to run the GUI on a different port.
341
+ """
342
+ gui_path = Path(__file__).parent / "agents" / "gui" / "agent_gui.py"
343
+ gui_path = gui_path.resolve()
344
+ cmd = f"streamlit run {gui_path}"
345
+
346
+ if port is not None and isinstance(port, int):
347
+ cmd += f" --server.port {port}"
348
+
349
+ _run_command(cmd, check_output=True)
350
+
351
+
352
+ def agent_client(tool_name: str, kwargs: str = None):
353
+ """Agent Client.
354
+
355
+ Parameters.
356
+ ----------
357
+ tool_name : str
358
+ Name of the tool
359
+ kwargs : str, optional
360
+ A stringfied JSON containing the kwargs for the tool, if needed.
361
+ """
362
+ print(f"Going to run agent tool '{tool_name}'.")
363
+ if kwargs:
364
+ try:
365
+ kwargs = json.loads(kwargs)
366
+ print(f"Using kwargs: {kwargs}")
367
+ except Exception as e:
368
+ print(f"Could not parse kwargs as a valid JSON: {kwargs}")
369
+ print(e)
370
+ print("-----------------")
371
+ from flowcept.agents.agent_client import run_tool
372
+
373
+ result = run_tool(tool_name, kwargs)[0]
374
+
375
+ print(result)
376
+
377
+
378
+ def check_services():
379
+ """
380
+ Run a full diagnostic test on the Flowcept system and its dependencies.
381
+
382
+ This function:
383
+ - Prints the current configuration path.
384
+ - Checks if required services (e.g., MongoDB, agent) are alive.
385
+ - Runs a test function wrapped with Flowcept instrumentation.
386
+ - Verifies MongoDB insertion (if enabled).
387
+ - Verifies agent communication and LLM connectivity (if enabled).
388
+
389
+ Returns
390
+ -------
391
+ None
392
+ Prints diagnostics to stdout; returns nothing.
393
+ """
394
+ from flowcept import Flowcept
395
+
396
+ print(f"Testing with settings at: {configs.SETTINGS_PATH}")
397
+ from flowcept.configs import MONGO_ENABLED, AGENT, KVDB_ENABLED
398
+
399
+ if not Flowcept.services_alive():
400
+ print("Some of the enabled services are not alive!")
401
+ return
402
+
403
+ check_safe_stops = KVDB_ENABLED
404
+
405
+ from uuid import uuid4
406
+ from flowcept.instrumentation.flowcept_task import flowcept_task
407
+
408
+ workflow_id = str(uuid4())
409
+
410
+ @flowcept_task
411
+ def test_function(n: int) -> Dict[str, int]:
412
+ return {"output": n + 1}
413
+
414
+ with Flowcept(workflow_id=workflow_id, check_safe_stops=check_safe_stops):
415
+ test_function(2)
416
+
417
+ if MONGO_ENABLED:
418
+ print("MongoDB is enabled, so we are testing it too.")
419
+ tasks = Flowcept.db.query({"workflow_id": workflow_id})
420
+ if len(tasks) != 1:
421
+ print(f"The query result, {len(tasks)}, is not what we expected.")
422
+ return
423
+
424
+ if AGENT.get("enabled", False):
425
+ print("Agent is enabled, so we are testing it too.")
426
+ from flowcept.agents.agent_client import run_tool
427
+
428
+ try:
429
+ print(run_tool("check_liveness"))
430
+ except Exception as e:
431
+ print(e)
432
+ return
433
+
434
+ print("Testing LLM connectivity")
435
+ check_llm_result = run_tool("check_llm")[0]
436
+ print(check_llm_result)
437
+
438
+ if "error" in check_llm_result.lower():
439
+ print("There is an error with the LLM communication.")
440
+ return
441
+ # TODO: the following needs to be fixed
442
+ # elif MONGO_ENABLED:
443
+ #
444
+ # print("Testing if llm chat was stored in MongoDB.")
445
+ # response_metadata = json.loads(check_llm_result.split("\n")[0])
446
+ # print(response_metadata)
447
+ # sleep(INSERTION_BUFFER_TIME * 1.05)
448
+ # chats = Flowcept.db.query({"workflow_id": response_metadata["agent_id"]})
449
+ # if chats:
450
+ # print(chats)
451
+ # else:
452
+ # print("Could not find chat history. Make sure that the DB Inserter service is on.")
453
+ print("\n\nAll expected services seem to be working properly!")
454
+ return
455
+
456
+
457
+ def start_mongo() -> None:
132
458
  """
133
- query = json.loads(query_str)
134
- print(Flowcept.db.query(query))
459
+ Start a MongoDB server using paths configured in the settings file.
460
+
461
+ Looks up:
462
+ databases:
463
+ mongodb:
464
+ - bin : str (required) path to the mongod executable
465
+ - log_path : str, optional (adds --fork --logpath)
466
+ - lock_file_path : str, optional (adds --pidfilepath)
467
+
468
+ Builds and runs the startup command.
469
+ """
470
+ # Safe nested gets
471
+ settings = getattr(configs, "settings", {}) or {}
472
+ databases = settings.get("databases") or {}
473
+ mongodb = databases.get("mongodb") or {}
474
+
475
+ bin_path = mongodb.get("bin")
476
+ log_path = mongodb.get("log_path")
477
+ lock_file_path = mongodb.get("lock_file_path")
478
+
479
+ if not bin_path:
480
+ print("Error: settings['databases']['mongodb']['bin'] is required.")
481
+ return
482
+
483
+ # Build command
484
+ parts = [shlex.quote(str(bin_path))]
485
+ if log_path:
486
+ parts += ["--fork", "--logpath", shlex.quote(str(log_path))]
487
+ if lock_file_path:
488
+ parts += ["--pidfilepath", shlex.quote(str(lock_file_path))]
489
+
490
+ cmd = " ".join(parts)
491
+ try:
492
+ out = _run_command(cmd, check_output=True)
493
+ if out:
494
+ print(out)
495
+ except subprocess.CalledProcessError as e:
496
+ print(f"Failed to start MongoDB: {e}")
497
+
498
+
499
+ def start_redis() -> None:
500
+ """
501
+ Start a Redis server using paths configured in settings.
502
+
503
+ Looks up:
504
+ mq:
505
+ - bin : str (required) path to the redis-server executable
506
+ - conf_file : str, optional (appended as the sole argument)
507
+
508
+ Builds and runs the command via _run_command(cmd, check_output=True).
509
+ """
510
+ settings = getattr(configs, "settings", {}) or {}
511
+ mq = settings.get("mq") or {}
512
+
513
+ if mq.get("type", None) != "redis":
514
+ print("Your settings file needs to specify redis as the MQ type. Please fix it.")
515
+ return
516
+
517
+ bin_path = mq.get("bin")
518
+ conf_file = mq.get("conf_file", None)
519
+
520
+ if not bin_path:
521
+ print("Error: settings['mq']['bin'] is required.")
522
+ return
523
+
524
+ parts = [shlex.quote(str(bin_path))]
525
+ if conf_file:
526
+ parts.append(shlex.quote(str(conf_file)))
527
+
528
+ cmd = " ".join(parts)
529
+ try:
530
+ out = _run_command(cmd, check_output=True)
531
+ if out:
532
+ print(out)
533
+ except subprocess.CalledProcessError as e:
534
+ print(f"Failed to start Redis: {e}")
135
535
 
136
536
 
137
537
  COMMAND_GROUPS = [
138
- ("Basic Commands", [show_config, start_services, stop_services]),
139
- ("Consumption Commands", [start_consumption_services, stop_consumption_services]),
140
- ("Database Commands", [workflow_count, query]),
538
+ ("Basic Commands", [version, check_services, show_settings, init_settings, start_services, stop_services]),
539
+ ("Consumption Commands", [start_consumption_services, stop_consumption_services, stream_messages]),
540
+ ("Database Commands", [workflow_count, query, get_task]),
541
+ ("Agent Commands", [start_agent, agent_client, start_agent_gui]),
542
+ ("External Services", [start_mongo, start_redis]),
141
543
  ]
142
544
 
143
545
  COMMANDS = set(f for _, fs in COMMAND_GROUPS for f in fs)
144
546
 
145
547
 
548
+ def _run_command(cmd_str: str, check_output: bool = True, popen_kwargs: Optional[Dict] = None) -> Optional[str]:
549
+ """
550
+ Run a shell command with optional output capture.
551
+
552
+ Parameters
553
+ ----------
554
+ cmd_str : str
555
+ The command to execute.
556
+ check_output : bool, optional
557
+ If True, capture and return the command's standard output.
558
+ If False, run interactively (stdout/stderr goes to terminal).
559
+ popen_kwargs : dict, optional
560
+ Extra keyword arguments to pass to subprocess.run.
561
+
562
+ Returns
563
+ -------
564
+ output : str or None
565
+ The standard output of the command if check_output is True, else None.
566
+
567
+ Raises
568
+ ------
569
+ subprocess.CalledProcessError
570
+ If the command exits with a non-zero status.
571
+ """
572
+ if popen_kwargs is None:
573
+ popen_kwargs = {}
574
+
575
+ kwargs = {"shell": True, "check": True, **popen_kwargs}
576
+ print(f"Going to run shell command:\n{cmd_str}")
577
+ if check_output:
578
+ kwargs.update({"capture_output": True, "text": True})
579
+ result = subprocess.run(cmd_str, **kwargs)
580
+ return result.stdout.strip()
581
+ else:
582
+ subprocess.run(cmd_str, **kwargs)
583
+ return None
584
+
585
+
146
586
  def _parse_numpy_doc(docstring: str):
147
587
  parsed = {}
148
588
  lines = docstring.splitlines() if docstring else []
@@ -178,8 +618,9 @@ def main(): # noqa: D103
178
618
  for pname, param in inspect.signature(func).parameters.items():
179
619
  arg_name = f"--{pname.replace('_', '-')}"
180
620
  params_doc = _parse_numpy_doc(doc).get(pname, {})
621
+
181
622
  help_text = f"{params_doc.get('type', '')} - {params_doc.get('desc', '').strip()}"
182
- if isinstance(param.annotation, bool):
623
+ if param.annotation is bool:
183
624
  parser.add_argument(arg_name, action="store_true", help=help_text)
184
625
  elif param.annotation == List[str]:
185
626
  parser.add_argument(arg_name, type=lambda s: s.split(","), help=help_text)
@@ -187,7 +628,7 @@ def main(): # noqa: D103
187
628
  parser.add_argument(arg_name, type=str, help=help_text)
188
629
 
189
630
  # Handle --help --command
190
- help_flag = "--help" in sys.argv
631
+ help_flag = "--help" in sys.argv or "-h" in sys.argv
191
632
  command_flags = {f"--{f.__name__.replace('_', '-')}" for f in COMMANDS}
192
633
  matched_command_flag = next((arg for arg in sys.argv if arg in command_flags), None)
193
634
 
@@ -203,7 +644,7 @@ def main(): # noqa: D103
203
644
  meta = params.get(pname, {})
204
645
  opt = p.default != inspect.Parameter.empty
205
646
  print(
206
- f" --{pname:<18} {meta.get('type', 'str')}, "
647
+ f" --{pname.replace('_', '-'):<18} {meta.get('type', 'str')}, "
207
648
  f"{'optional' if opt else 'required'} - {meta.get('desc', '').strip()}"
208
649
  )
209
650
  print()
@@ -231,7 +672,7 @@ def main(): # noqa: D103
231
672
  opt = sig.parameters[argname].default != inspect.Parameter.empty
232
673
  print(
233
674
  f" --"
234
- f"{argname:<18} {meta['type']}, "
675
+ f"{argname.replace('_', '-'):<18} {meta['type']}, "
235
676
  f"{'optional' if opt else 'required'} - {meta['desc'].strip()}"
236
677
  )
237
678
  print()
@@ -258,3 +699,4 @@ def main(): # noqa: D103
258
699
 
259
700
  if __name__ == "__main__":
260
701
  main()
702
+ # check_services()
@@ -707,6 +707,53 @@ class MongoDBDAO(DocumentDBDAO):
707
707
  else:
708
708
  raise Exception(f"You used type={collection}, but MongoDB only stores tasks, workflows, and objects")
709
709
 
710
+ def raw_task_pipeline(self, pipeline: List[Dict]):
711
+ """
712
+ Run a raw MongoDB aggregation pipeline on the tasks collection.
713
+
714
+ This method allows advanced users to directly execute an
715
+ aggregation pipeline against the underlying ``_tasks_collection``.
716
+ It is intended for cases where more complex queries, transformations,
717
+ or aggregations are needed beyond the high-level query APIs.
718
+
719
+ Parameters
720
+ ----------
721
+ pipeline : list of dict
722
+ A MongoDB aggregation pipeline represented as a list of
723
+ stage documents (e.g., ``[{"$match": {...}}, {"$group": {...}}]``).
724
+
725
+ Returns
726
+ -------
727
+ list of dict or None
728
+ The aggregation results as a list of documents if successful,
729
+ or ``None`` if an error occurred.
730
+
731
+ Raises
732
+ ------
733
+ Exception
734
+ Any exception raised by the underlying MongoDB driver will be
735
+ logged and the method will return ``None`` instead of propagating.
736
+
737
+ Examples
738
+ --------
739
+ Count the number of tasks per workflow:
740
+
741
+ >>> pipeline = [
742
+ ... {"$group": {"_id": "$workflow_id", "count": {"$sum": 1}}}
743
+ ... ]
744
+ >>> results = obj.raw_task_pipeline(pipeline)
745
+ >>> for r in results:
746
+ ... print(r["_id"], r["count"])
747
+ wf_123 42
748
+ wf_456 18
749
+ """
750
+ try:
751
+ rs = self._tasks_collection.aggregate(pipeline)
752
+ return list(rs)
753
+ except Exception as e:
754
+ self.logger.exception(e)
755
+ return None
756
+
710
757
  def task_query(
711
758
  self,
712
759
  filter: Dict = None,
@@ -1,6 +1,6 @@
1
1
  """Key value module."""
2
2
 
3
- from redis import Redis, ConnectionPool
3
+ from flowcept.commons.daos.redis_conn import RedisConn
4
4
 
5
5
  from flowcept.commons.flowcept_logger import FlowceptLogger
6
6
  from flowcept.configs import (
@@ -24,32 +24,13 @@ class KeyValueDAO:
24
24
  cls._instance = super(KeyValueDAO, cls).__new__(cls)
25
25
  return cls._instance
26
26
 
27
- @staticmethod
28
- def build_redis_conn_pool():
29
- """Utility function to build Redis connection."""
30
- pool = ConnectionPool(
31
- host=KVDB_HOST,
32
- port=KVDB_PORT,
33
- db=0,
34
- password=KVDB_PASSWORD,
35
- decode_responses=False,
36
- max_connections=10000, # TODO: Config file
37
- socket_keepalive=True,
38
- retry_on_timeout=True,
39
- )
40
- return Redis(connection_pool=pool)
41
- # return Redis()
42
-
43
27
  def __init__(self):
44
28
  if not hasattr(self, "_initialized"):
45
29
  self._initialized = True
46
30
  self.logger = FlowceptLogger()
47
- if KVDB_URI is not None:
48
- # If a URI is provided, use it for connection
49
- self.redis_conn = Redis.from_url(KVDB_URI)
50
- else:
51
- # Otherwise, use the host, port, and password settings
52
- self.redis_conn = KeyValueDAO.build_redis_conn_pool()
31
+ self.redis_conn = RedisConn.build_redis_conn_pool(
32
+ host=KVDB_HOST, port=KVDB_PORT, password=KVDB_PASSWORD, uri=KVDB_URI
33
+ )
53
34
 
54
35
  def delete_set(self, set_name: str):
55
36
  """Delete it."""
@@ -133,3 +114,18 @@ class KeyValueDAO:
133
114
  None
134
115
  """
135
116
  self.redis_conn.delete(key)
117
+
118
+ def liveness_test(self):
119
+ """Get the livelyness of it."""
120
+ try:
121
+ response = self.redis_conn.ping()
122
+ if response:
123
+ return True
124
+ else:
125
+ return False
126
+ except ConnectionError as e:
127
+ self.logger.exception(e)
128
+ return False
129
+ except Exception as e:
130
+ self.logger.exception(e)
131
+ return False