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.
- flowcept/__init__.py +7 -4
- flowcept/agents/__init__.py +5 -0
- flowcept/agents/agent_client.py +58 -0
- flowcept/agents/agents_utils.py +181 -0
- flowcept/agents/dynamic_schema_tracker.py +191 -0
- flowcept/agents/flowcept_agent.py +30 -0
- flowcept/agents/flowcept_ctx_manager.py +175 -0
- flowcept/agents/gui/__init__.py +5 -0
- flowcept/agents/gui/agent_gui.py +76 -0
- flowcept/agents/gui/gui_utils.py +239 -0
- flowcept/agents/llms/__init__.py +1 -0
- flowcept/agents/llms/claude_gcp.py +139 -0
- flowcept/agents/llms/gemini25.py +119 -0
- flowcept/agents/prompts/__init__.py +1 -0
- flowcept/agents/prompts/general_prompts.py +69 -0
- flowcept/agents/prompts/in_memory_query_prompts.py +297 -0
- flowcept/agents/tools/__init__.py +1 -0
- flowcept/agents/tools/general_tools.py +102 -0
- flowcept/agents/tools/in_memory_queries/__init__.py +1 -0
- flowcept/agents/tools/in_memory_queries/in_memory_queries_tools.py +704 -0
- flowcept/agents/tools/in_memory_queries/pandas_agent_utils.py +309 -0
- flowcept/cli.py +459 -17
- flowcept/commons/daos/docdb_dao/mongodb_dao.py +47 -0
- flowcept/commons/daos/keyvalue_dao.py +19 -23
- flowcept/commons/daos/mq_dao/mq_dao_base.py +49 -38
- flowcept/commons/daos/mq_dao/mq_dao_kafka.py +20 -3
- flowcept/commons/daos/mq_dao/mq_dao_mofka.py +4 -0
- flowcept/commons/daos/mq_dao/mq_dao_redis.py +38 -5
- flowcept/commons/daos/redis_conn.py +47 -0
- flowcept/commons/flowcept_dataclasses/task_object.py +50 -27
- flowcept/commons/flowcept_dataclasses/workflow_object.py +9 -1
- flowcept/commons/settings_factory.py +2 -4
- flowcept/commons/task_data_preprocess.py +400 -0
- flowcept/commons/utils.py +26 -7
- flowcept/configs.py +48 -29
- flowcept/flowcept_api/flowcept_controller.py +102 -18
- flowcept/flowceptor/adapters/base_interceptor.py +24 -11
- flowcept/flowceptor/adapters/brokers/__init__.py +1 -0
- flowcept/flowceptor/adapters/brokers/mqtt_interceptor.py +132 -0
- flowcept/flowceptor/adapters/mlflow/mlflow_interceptor.py +3 -3
- flowcept/flowceptor/adapters/tensorboard/tensorboard_interceptor.py +3 -3
- flowcept/flowceptor/consumers/agent/__init__.py +1 -0
- flowcept/flowceptor/consumers/agent/base_agent_context_manager.py +125 -0
- flowcept/flowceptor/consumers/base_consumer.py +94 -0
- flowcept/flowceptor/consumers/consumer_utils.py +5 -4
- flowcept/flowceptor/consumers/document_inserter.py +135 -36
- flowcept/flowceptor/telemetry_capture.py +6 -3
- flowcept/instrumentation/flowcept_agent_task.py +294 -0
- flowcept/instrumentation/flowcept_decorator.py +43 -0
- flowcept/instrumentation/flowcept_loop.py +3 -3
- flowcept/instrumentation/flowcept_task.py +64 -24
- flowcept/instrumentation/flowcept_torch.py +5 -5
- flowcept/instrumentation/task_capture.py +87 -4
- flowcept/version.py +1 -1
- {flowcept-0.8.10.dist-info → flowcept-0.8.12.dist-info}/METADATA +48 -11
- flowcept-0.8.12.dist-info/RECORD +101 -0
- resources/sample_settings.yaml +46 -14
- flowcept/flowceptor/adapters/zambeze/__init__.py +0 -1
- flowcept/flowceptor/adapters/zambeze/zambeze_dataclasses.py +0 -41
- flowcept/flowceptor/adapters/zambeze/zambeze_interceptor.py +0 -102
- flowcept-0.8.10.dist-info/RECORD +0 -75
- {flowcept-0.8.10.dist-info → flowcept-0.8.12.dist-info}/WHEEL +0 -0
- {flowcept-0.8.10.dist-info → flowcept-0.8.12.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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", [
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|