flowcept 0.9.18__py3-none-any.whl → 0.9.19__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.
@@ -1,4 +1,6 @@
1
1
  import asyncio
2
+ import json
3
+ import re
2
4
  from typing import Dict, List, Callable
3
5
 
4
6
  from flowcept.configs import AGENT_HOST, AGENT_PORT
@@ -48,10 +50,14 @@ def run_tool(
48
50
  result: List[TextContent] = await session.call_tool(tool_name, arguments=kwargs)
49
51
  actual_result = []
50
52
  for r in result.content:
51
- if isinstance(r, str):
52
- actual_result.append(r)
53
- else:
54
- actual_result.append(r.text)
53
+ text = r if isinstance(r, str) else r.text
54
+ try:
55
+ json.loads(text)
56
+ actual_result.append(text)
57
+ except Exception:
58
+ match = re.search(r"Error code:\\s*(\\d+)", text)
59
+ code = int(match.group(1)) if match else 400
60
+ actual_result.append(json.dumps({"code": code, "result": text, "tool_name": tool_name}))
55
61
 
56
62
  return actual_result
57
63
 
@@ -139,8 +139,8 @@ def build_llm_model(
139
139
  if _service_provider == "sambanova":
140
140
  from langchain_community.llms.sambanova import SambaStudio
141
141
 
142
- os.environ["SAMBASTUDIO_URL"] = AGENT.get("llm_server_url")
143
- os.environ["SAMBASTUDIO_API_KEY"] = AGENT.get("api_key")
142
+ os.environ["SAMBASTUDIO_URL"] = os.environ.get("SAMBASTUDIO_URL", AGENT.get("llm_server_url"))
143
+ os.environ["SAMBASTUDIO_API_KEY"] = os.environ.get("SAMBASTUDIO_API_KEY", AGENT.get("api_key"))
144
144
 
145
145
  llm = SambaStudio(model_kwargs=_model_kwargs)
146
146
  elif _service_provider == "azure":
@@ -155,7 +155,16 @@ def build_llm_model(
155
155
  from langchain_openai import ChatOpenAI
156
156
 
157
157
  api_key = os.environ.get("OPENAI_API_KEY", AGENT.get("api_key", None))
158
- llm = ChatOpenAI(openai_api_key=api_key, **model_kwargs)
158
+ base_url = os.environ.get("OPENAI_BASE_URL", AGENT.get("llm_server_url") or None)
159
+ org = os.environ.get("OPENAI_ORG_ID", AGENT.get("organization", None))
160
+
161
+ init_kwargs = {"api_key": api_key}
162
+ if base_url:
163
+ init_kwargs["base_url"] = base_url
164
+ if org:
165
+ init_kwargs["organization"] = org
166
+
167
+ llm = ChatOpenAI(**init_kwargs, **_model_kwargs)
159
168
  elif _service_provider == "google":
160
169
  if "claude" in _model_kwargs["model"]:
161
170
  api_key = os.environ.get("GOOGLE_API_KEY", AGENT.get("api_key", None))
@@ -168,22 +177,6 @@ def build_llm_model(
168
177
  from flowcept.agents.llms.gemini25 import Gemini25LLM
169
178
 
170
179
  llm = Gemini25LLM(**_model_kwargs)
171
- elif _service_provider == "openai":
172
- from langchain_openai import ChatOpenAI
173
-
174
- api_key = os.environ.get("OPENAI_API_KEY", AGENT.get("api_key"))
175
- base_url = os.environ.get("OPENAI_BASE_URL", AGENT.get("llm_server_url") or None) # optional
176
- org = os.environ.get("OPENAI_ORG_ID", AGENT.get("organization", None)) # optional
177
-
178
- init_kwargs = {"api_key": api_key}
179
- if base_url:
180
- init_kwargs["base_url"] = base_url
181
- if org:
182
- init_kwargs["organization"] = org
183
-
184
- # IMPORTANT: use the merged kwargs so `model` and temps flow through
185
- llm = ChatOpenAI(**init_kwargs, **_model_kwargs)
186
-
187
180
  else:
188
181
  raise Exception("Currently supported providers are sambanova, openai, azure, and google.")
189
182
  if track_tools:
@@ -1,32 +1,133 @@
1
+ import json
2
+ import os
1
3
  from threading import Thread
2
- from time import sleep
3
4
 
4
5
  from flowcept.agents import check_liveness
6
+ from flowcept.agents.agents_utils import ToolResult
7
+ from flowcept.agents.tools.general_tools import prompt_handler
5
8
  from flowcept.agents.agent_client import run_tool
6
- from flowcept.agents.flowcept_ctx_manager import mcp_flowcept
7
- from flowcept.configs import AGENT_HOST, AGENT_PORT
8
- from flowcept.flowcept_api.flowcept_controller import Flowcept
9
+ from flowcept.agents.flowcept_ctx_manager import mcp_flowcept, ctx_manager
10
+ from flowcept.commons.flowcept_logger import FlowceptLogger
11
+ from flowcept.configs import AGENT_HOST, AGENT_PORT, DUMP_BUFFER_PATH, MQ_ENABLED
12
+ from flowcept.flowceptor.consumers.agent.base_agent_context_manager import BaseAgentContextManager
13
+ from uuid import uuid4
9
14
 
10
15
  import uvicorn
11
16
 
12
17
 
13
- def main():
18
+ class FlowceptAgent:
14
19
  """
15
- Start the MCP server.
20
+ Flowcept agent server wrapper with optional offline buffer loading.
16
21
  """
17
- f = Flowcept(start_persistence=False, save_workflow=False, check_safe_stops=False).start()
18
- f.logger.info(f"This section's workflow_id={Flowcept.current_workflow_id}")
19
22
 
20
- def run():
21
- uvicorn.run(mcp_flowcept.streamable_http_app, host=AGENT_HOST, port=AGENT_PORT, lifespan="on")
23
+ def __init__(self, buffer_path: str | None = None):
24
+ """
25
+ Initialize a FlowceptAgent.
26
+
27
+ Parameters
28
+ ----------
29
+ buffer_path : str or None
30
+ Optional path to a JSONL buffer file. When MQ is disabled, the agent
31
+ loads this file once at startup.
32
+ """
33
+ self.buffer_path = buffer_path
34
+ self.logger = FlowceptLogger()
35
+ self._server_thread: Thread | None = None
36
+ self._server = None
37
+
38
+ def _load_buffer_once(self) -> int:
39
+ """
40
+ Load messages from a JSONL buffer file into the agent context.
41
+
42
+ Returns
43
+ -------
44
+ int
45
+ Number of messages loaded.
46
+ """
47
+ path = self.buffer_path or DUMP_BUFFER_PATH
48
+ if not os.path.exists(path):
49
+ raise FileNotFoundError(f"Buffer file not found: {path}")
50
+
51
+ count = 0
52
+ self.logger.info(f"Loading agent buffer from {path}")
53
+ if ctx_manager.agent_id is None:
54
+ agent_id = str(uuid4())
55
+ BaseAgentContextManager.agent_id = agent_id
56
+ ctx_manager.agent_id = agent_id
57
+ with open(path, "r") as handle:
58
+ for line in handle:
59
+ line = line.strip()
60
+ if not line:
61
+ continue
62
+ msg_obj = json.loads(line)
63
+ ctx_manager.message_handler(msg_obj)
64
+ count += 1
65
+ self.logger.info(f"Loaded {count} messages from buffer.")
66
+ return count
67
+
68
+ def _run_server(self):
69
+ """Run the MCP server (blocking call)."""
70
+ config = uvicorn.Config(mcp_flowcept.streamable_http_app, host=AGENT_HOST, port=AGENT_PORT, lifespan="on")
71
+ self._server = uvicorn.Server(config)
72
+ self._server.run()
73
+
74
+ def start(self):
75
+ """
76
+ Start the agent server in a background thread.
77
+
78
+ Returns
79
+ -------
80
+ FlowceptAgent
81
+ The current instance.
82
+ """
83
+ if not MQ_ENABLED:
84
+ self._load_buffer_once()
85
+
86
+ self._server_thread = Thread(target=self._run_server, daemon=False)
87
+ self._server_thread.start()
88
+ self.logger.info(f"Flowcept agent server started on {AGENT_HOST}:{AGENT_PORT}")
89
+ return self
22
90
 
23
- server_thread = Thread(target=run, daemon=False)
24
- server_thread.start()
25
- sleep(2)
91
+ def stop(self):
92
+ """Stop the agent server and wait briefly for shutdown."""
93
+ if self._server is not None:
94
+ self._server.should_exit = True
95
+ if self._server_thread is not None:
96
+ self._server_thread.join(timeout=5)
97
+
98
+ def wait(self):
99
+ """Block until the server thread exits."""
100
+ if self._server_thread is not None:
101
+ self._server_thread.join()
102
+
103
+ def query(self, message: str) -> ToolResult:
104
+ """
105
+ Send a prompt to the agent's main router tool and return the response.
106
+ """
107
+ try:
108
+ resp = run_tool(tool_name=prompt_handler, kwargs={"message": message})[0]
109
+ except Exception as e:
110
+ return ToolResult(code=400, result=f"Error executing tool prompt_handler: {e}", tool_name="prompt_handler")
111
+
112
+ try:
113
+ return ToolResult(**json.loads(resp))
114
+ except Exception as e:
115
+ return ToolResult(
116
+ code=499,
117
+ result=f"Could not parse tool response as JSON: {resp}",
118
+ extra=str(e),
119
+ tool_name="prompt_handler",
120
+ )
121
+
122
+
123
+ def main():
124
+ """
125
+ Start the MCP server.
126
+ """
127
+ agent = FlowceptAgent().start()
26
128
  # Wake up tool call
27
129
  print(run_tool(check_liveness, host=AGENT_HOST, port=AGENT_PORT)[0])
28
-
29
- server_thread.join()
130
+ agent.wait()
30
131
 
31
132
 
32
133
  if __name__ == "__main__":
@@ -103,7 +103,7 @@ class FlowceptAgentContextManager(BaseAgentContextManager):
103
103
  self.schema_tracker = DynamicSchemaTracker(**self.tracker_config)
104
104
  self.msgs_counter = 0
105
105
  self.context_chunk_size = 1 # Should be in the settings
106
- super().__init__()
106
+ super().__init__(allow_mq_disabled=True)
107
107
 
108
108
  def message_handler(self, msg_obj: Dict):
109
109
  """
@@ -133,12 +133,15 @@ class FlowceptAgentContextManager(BaseAgentContextManager):
133
133
  if task_msg.activity_id == "reset_user_context":
134
134
  self.context.reset_context()
135
135
  self.msgs_counter = 0
136
- FlowceptTask(
137
- agent_id=self.agent_id,
138
- generated={"msg": "Provenance Agent reset context."},
139
- subtype="agent_task",
140
- activity_id="reset_user_context",
141
- ).send()
136
+ if self._mq_dao is None:
137
+ self.logger.warning("MQ is disabled; skipping reset_user_context response message.")
138
+ else:
139
+ FlowceptTask(
140
+ agent_id=self.agent_id,
141
+ generated={"msg": "Provenance Agent reset context."},
142
+ subtype="agent_task",
143
+ activity_id="reset_user_context",
144
+ ).send()
142
145
  return True
143
146
  elif task_msg.activity_id == "provenance_query":
144
147
  self.logger.info("Received a prov query message!")
@@ -161,14 +164,17 @@ class FlowceptAgentContextManager(BaseAgentContextManager):
161
164
  status = Status.ERROR
162
165
  error = f"Could not convert the following into a ToolResult:\n{resp}\nException: {e}"
163
166
  generated = {"text": str(resp)}
164
- FlowceptTask(
165
- agent_id=self.agent_id,
166
- generated=generated,
167
- stderr=error,
168
- status=status,
169
- subtype="agent_task",
170
- activity_id="provenance_query_response",
171
- ).send()
167
+ if self._mq_dao is None:
168
+ self.logger.warning("MQ is disabled; skipping provenance_query response message.")
169
+ else:
170
+ FlowceptTask(
171
+ agent_id=self.agent_id,
172
+ generated=generated,
173
+ stderr=error,
174
+ status=status,
175
+ subtype="agent_task",
176
+ activity_id="provenance_query_response",
177
+ ).send()
172
178
 
173
179
  return True
174
180
 
@@ -200,12 +206,10 @@ class FlowceptAgentContextManager(BaseAgentContextManager):
200
206
  ]
201
207
  )
202
208
  except Exception as e:
203
- self.logger.error(
204
- f"Could not add these tasks to buffer!\n"
205
- f"{
206
- self.context.task_summaries[self.msgs_counter - self.context_chunk_size : self.msgs_counter]
207
- }"
208
- )
209
+ task_slice = self.context.task_summaries[
210
+ self.msgs_counter - self.context_chunk_size : self.msgs_counter
211
+ ]
212
+ self.logger.error(f"Could not add these tasks to buffer!\n{task_slice}")
209
213
  self.logger.exception(e)
210
214
 
211
215
  # self.monitor_chunk()
@@ -232,9 +236,12 @@ class FlowceptAgentContextManager(BaseAgentContextManager):
232
236
  if len(result):
233
237
  content = result[0].text
234
238
  if content != "Error executing tool":
235
- msg = {"type": "flowcept_agent", "info": "monitor", "content": content}
236
- self._mq_dao.send_message(msg)
237
- self.logger.debug(str(content))
239
+ if self._mq_dao is None:
240
+ self.logger.warning("MQ is disabled; skipping monitor message.")
241
+ else:
242
+ msg = {"type": "flowcept_agent", "info": "monitor", "content": content}
243
+ self._mq_dao.send_message(msg)
244
+ self.logger.debug(str(content))
238
245
  else:
239
246
  self.logger.error(content)
240
247
 
@@ -1,7 +1,5 @@
1
1
  """Key value module."""
2
2
 
3
- from flowcept.commons.daos.redis_conn import RedisConn
4
-
5
3
  from flowcept.commons.flowcept_logger import FlowceptLogger
6
4
  from flowcept.configs import (
7
5
  KVDB_HOST,
@@ -26,12 +24,23 @@ class KeyValueDAO:
26
24
 
27
25
  def __init__(self):
28
26
  if not hasattr(self, "_initialized"):
29
- self._initialized = True
30
27
  self.logger = FlowceptLogger()
28
+ from flowcept.commons.daos.redis_conn import RedisConn
29
+
31
30
  self.redis_conn = RedisConn.build_redis_conn_pool(
32
31
  host=KVDB_HOST, port=KVDB_PORT, password=KVDB_PASSWORD, uri=KVDB_URI
33
32
  )
34
33
 
34
+ self._initialized = True
35
+
36
+ @staticmethod
37
+ def get_set_name(set_id: str, exec_bundle_id=None) -> str:
38
+ """Return a consistent set name for KVDB sets."""
39
+ set_name = set_id
40
+ if exec_bundle_id is not None:
41
+ set_name += "_" + str(exec_bundle_id)
42
+ return set_name
43
+
35
44
  def delete_set(self, set_name: str):
36
45
  """Delete it."""
37
46
  self.redis_conn.delete(set_name)
@@ -7,6 +7,7 @@ import msgpack
7
7
  from time import time
8
8
  import flowcept.commons
9
9
  from flowcept.commons.autoflush_buffer import AutoflushBuffer
10
+ from flowcept.commons.daos.keyvalue_dao import KeyValueDAO
10
11
  from flowcept.commons.utils import chunked
11
12
  from flowcept.commons.flowcept_logger import FlowceptLogger
12
13
  from flowcept.configs import (
@@ -29,6 +30,8 @@ class MQDao(object):
29
30
 
30
31
  ENCODER = GenericJSONEncoder if JSON_SERIALIZER == "complex" else None
31
32
  # TODO we don't have a unit test to cover complex dict!
33
+ MQ_THREAD_SET_ID = "started_mq_thread_execution"
34
+ MQ_FLUSH_COMPLETE_SET_ID = "pending_mq_flush_complete"
32
35
 
33
36
  @staticmethod
34
37
  def build(*args, **kwargs) -> "MQDao":
@@ -51,20 +54,6 @@ class MQDao(object):
51
54
  else:
52
55
  raise NotImplementedError
53
56
 
54
- @staticmethod
55
- def _get_set_name(exec_bundle_id=None):
56
- """Get the set name.
57
-
58
- :param exec_bundle_id: A way to group one or many interceptors, and
59
- treat each group as a bundle to control when their time_based
60
- threads started and ended.
61
- :return:
62
- """
63
- set_id = "started_mq_thread_execution"
64
- if exec_bundle_id is not None:
65
- set_id += "_" + str(exec_bundle_id)
66
- return set_id
67
-
68
57
  def __init__(self, adapter_settings=None):
69
58
  self.logger = FlowceptLogger()
70
59
  self.started = False
@@ -103,22 +92,36 @@ class MQDao(object):
103
92
 
104
93
  def register_time_based_thread_init(self, interceptor_instance_id: str, exec_bundle_id=None):
105
94
  """Register the time."""
106
- set_name = MQDao._get_set_name(exec_bundle_id)
95
+ set_name = KeyValueDAO.get_set_name(MQDao.MQ_THREAD_SET_ID, exec_bundle_id)
107
96
  # self.logger.info(
108
97
  # f"Register start of time_based MQ flush thread {set_name}.{interceptor_instance_id}"
109
98
  # )
110
99
  self._keyvalue_dao.add_key_into_set(set_name, interceptor_instance_id)
100
+ flush_set_name = KeyValueDAO.get_set_name(MQDao.MQ_FLUSH_COMPLETE_SET_ID, exec_bundle_id)
101
+ self._keyvalue_dao.add_key_into_set(flush_set_name, interceptor_instance_id)
111
102
 
112
103
  def register_time_based_thread_end(self, interceptor_instance_id: str, exec_bundle_id=None):
113
104
  """Register time."""
114
- set_name = MQDao._get_set_name(exec_bundle_id)
105
+ set_name = KeyValueDAO.get_set_name(MQDao.MQ_THREAD_SET_ID, exec_bundle_id)
115
106
  self.logger.info(f"Registering end of time_based MQ flush thread {set_name}.{interceptor_instance_id}")
116
107
  self._keyvalue_dao.remove_key_from_set(set_name, interceptor_instance_id)
117
108
  self.logger.info(f"Done registering time_based MQ flush thread {set_name}.{interceptor_instance_id}")
118
109
 
119
110
  def all_time_based_threads_ended(self, exec_bundle_id=None):
120
111
  """Get all time."""
121
- set_name = MQDao._get_set_name(exec_bundle_id)
112
+ set_name = KeyValueDAO.get_set_name(MQDao.MQ_THREAD_SET_ID, exec_bundle_id)
113
+ return self._keyvalue_dao.set_is_empty(set_name)
114
+
115
+ def register_flush_complete(self, interceptor_instance_id: str, exec_bundle_id=None):
116
+ """Register a flush-complete signal for an interceptor."""
117
+ set_name = KeyValueDAO.get_set_name(MQDao.MQ_FLUSH_COMPLETE_SET_ID, exec_bundle_id)
118
+ self.logger.info(f"Registering flush completion {set_name}.{interceptor_instance_id}")
119
+ self._keyvalue_dao.remove_key_from_set(set_name, interceptor_instance_id)
120
+ self.logger.info(f"Done registering flush completion {set_name}.{interceptor_instance_id}")
121
+
122
+ def all_flush_complete_received(self, exec_bundle_id=None):
123
+ """Return True when all interceptors in the bundle reported flush completion."""
124
+ set_name = KeyValueDAO.get_set_name(MQDao.MQ_FLUSH_COMPLETE_SET_ID, exec_bundle_id)
122
125
  return self._keyvalue_dao.set_is_empty(set_name)
123
126
 
124
127
  def set_campaign_id(self, campaign_id=None):
@@ -172,11 +175,14 @@ class MQDao(object):
172
175
  if self._time_based_flushing_started:
173
176
  self.buffer.stop()
174
177
  self._time_based_flushing_started = False
178
+ self.logger.debug("MQ time-based flushed for the last time!")
175
179
  else:
176
180
  self.logger.error("MQ time-based flushing is not started")
177
181
  else:
178
182
  self.buffer = list()
179
183
 
184
+ self.logger.debug("Buffer closed.")
185
+
180
186
  def _stop_timed(self, interceptor_instance_id: str, check_safe_stops: bool = True, bundle_exec_id: int = None):
181
187
  t1 = time()
182
188
  self._stop(interceptor_instance_id, check_safe_stops, bundle_exec_id)
@@ -190,10 +196,12 @@ class MQDao(object):
190
196
 
191
197
  def _stop(self, interceptor_instance_id: str = None, check_safe_stops: bool = True, bundle_exec_id: int = None):
192
198
  """Stop MQ publisher."""
193
- self.logger.debug(f"MQ pub received stop sign: bundle={bundle_exec_id}, interceptor={interceptor_instance_id}")
194
199
  self._close_buffer()
195
- self.logger.debug("Flushed MQ for the last time!")
196
- if check_safe_stops:
200
+ if check_safe_stops and MQ_ENABLED:
201
+ self.logger.debug(
202
+ f"Sending flush-complete msg. Bundle: {bundle_exec_id}; interceptor id: {interceptor_instance_id}"
203
+ )
204
+ self._send_mq_dao_flush_complete(interceptor_instance_id, bundle_exec_id)
197
205
  self.logger.debug(f"Sending stop msg. Bundle: {bundle_exec_id}; interceptor id: {interceptor_instance_id}")
198
206
  self._send_mq_dao_time_thread_stop(interceptor_instance_id, bundle_exec_id)
199
207
  self.started = False
@@ -210,6 +218,15 @@ class MQDao(object):
210
218
  # self.logger.info("Control msg sent: " + str(msg))
211
219
  self.send_message(msg)
212
220
 
221
+ def _send_mq_dao_flush_complete(self, interceptor_instance_id, exec_bundle_id=None):
222
+ msg = {
223
+ "type": "flowcept_control",
224
+ "info": "mq_flush_complete",
225
+ "interceptor_instance_id": interceptor_instance_id,
226
+ "exec_bundle_id": exec_bundle_id,
227
+ }
228
+ self.send_message(msg)
229
+
213
230
  def send_document_inserter_stop(self, exec_bundle_id=None):
214
231
  """Send the document."""
215
232
  # These control_messages are handled by the document inserter
flowcept/configs.py CHANGED
@@ -9,7 +9,7 @@ from flowcept.version import __version__
9
9
  PROJECT_NAME = "flowcept"
10
10
 
11
11
  DEFAULT_SETTINGS = {
12
- "version": __version__,
12
+ "flowcept_version": __version__,
13
13
  "log": {"log_file_level": "disable", "log_stream_level": "disable"},
14
14
  "project": {"dump_buffer": {"enabled": True}},
15
15
  "telemetry_capture": {},
@@ -81,7 +81,7 @@ FLOWCEPT_USER = settings["experiment"].get("user", "blank_user")
81
81
 
82
82
  MQ_INSTANCES = settings["mq"].get("instances", None)
83
83
  MQ_SETTINGS = settings["mq"]
84
- MQ_ENABLED = os.getenv("MQ_ENABLED", settings["mq"].get("enabled", True))
84
+ MQ_ENABLED = os.getenv("MQ_ENABLED", str(settings["mq"].get("enabled", True))).strip().lower() in _TRUE_VALUES
85
85
  MQ_TYPE = os.getenv("MQ_TYPE", settings["mq"].get("type", "redis"))
86
86
  MQ_CHANNEL = os.getenv("MQ_CHANNEL", settings["mq"].get("channel", "interception"))
87
87
  MQ_PASSWORD = settings["mq"].get("password", None)
@@ -103,6 +103,11 @@ KVDB_PORT = int(os.getenv("KVDB_PORT", settings["kv_db"].get("port", "6379")))
103
103
  KVDB_URI = os.getenv("KVDB_URI", settings["kv_db"].get("uri", None))
104
104
  KVDB_ENABLED = settings["kv_db"].get("enabled", False)
105
105
 
106
+ if MQ_ENABLED and not KVDB_ENABLED:
107
+ raise ValueError(
108
+ "Invalid configuration: MQ is enabled but kv_db is disabled. "
109
+ "Enable kv_db.enabled (and KVDB) when MQ is enabled."
110
+ )
106
111
 
107
112
  DATABASES = settings.get("databases", {})
108
113
 
@@ -160,6 +165,12 @@ JSON_SERIALIZER = settings["project"].get("json_serializer", "default")
160
165
  REPLACE_NON_JSON_SERIALIZABLE = settings["project"].get("replace_non_json_serializable", True)
161
166
  ENRICH_MESSAGES = settings["project"].get("enrich_messages", True)
162
167
 
168
+ if DB_FLUSH_MODE == "online" and not MQ_ENABLED:
169
+ raise ValueError(
170
+ "Invalid configuration: project.db_flush_mode is 'online' but MQ is disabled. "
171
+ "Enable mq.enabled (or MQ_ENABLED=true) or set project.db_flush_mode to 'offline'."
172
+ )
173
+
163
174
  # Default: enable dump buffer only when running in offline flush mode.
164
175
  _DEFAULT_DUMP_BUFFER_ENABLED = DB_FLUSH_MODE == "offline"
165
176
  DUMP_BUFFER_ENABLED = (
@@ -45,12 +45,12 @@ class BaseAgentContextManager(BaseConsumer):
45
45
 
46
46
  agent_id = None
47
47
 
48
- def __init__(self):
48
+ def __init__(self, allow_mq_disabled: bool = False):
49
49
  """
50
50
  Initializes the agent and resets its context state.
51
51
  """
52
52
  self._started = False
53
- super().__init__()
53
+ super().__init__(allow_mq_disabled=allow_mq_disabled)
54
54
  # self.context = BaseAppContext(tasks=[])
55
55
  self.agent_id = BaseAgentContextManager.agent_id
56
56
 
@@ -13,18 +13,28 @@ class BaseConsumer(object):
13
13
 
14
14
  This class provides a standard interface and shared logic for subscribing to
15
15
  message queues and dispatching messages to a handler.
16
+
17
+ Note
18
+ ----
19
+ The MQ-disabled path is only intended for agent consumers that can operate
20
+ from an offline buffer file. General consumers that require MQ should keep
21
+ the default behavior (raise when MQ_ENABLED is False).
16
22
  """
17
23
 
18
- def __init__(self):
24
+ def __init__(self, allow_mq_disabled: bool = False):
19
25
  """Initialize the message queue DAO and logger."""
26
+ self.logger = FlowceptLogger()
27
+ self._main_thread: Optional[Thread] = None
28
+
20
29
  if not MQ_ENABLED:
30
+ if allow_mq_disabled:
31
+ self._mq_dao = None
32
+ self.logger.warning("MQ is disabled; starting consumer without a message queue.")
33
+ return
21
34
  raise Exception("MQ is disabled in the settings. You cannot consume messages.")
22
35
 
23
36
  self._mq_dao = MQDao.build()
24
37
 
25
- self.logger = FlowceptLogger()
26
- self._main_thread: Optional[Thread] = None
27
-
28
38
  @abstractmethod
29
39
  def message_handler(self, msg_obj: Dict) -> bool:
30
40
  """
@@ -62,6 +72,9 @@ class BaseConsumer(object):
62
72
  BaseConsumer
63
73
  The current instance (to allow chaining).
64
74
  """
75
+ if self._mq_dao is None:
76
+ self.logger.warning("MQ is disabled; skipping message consumption start.")
77
+ return self
65
78
  if target is None:
66
79
  target = self.default_thread_target
67
80
  self._mq_dao.subscribe()
@@ -85,6 +98,9 @@ class BaseConsumer(object):
85
98
  --------
86
99
  start : Starts the consumer and optionally spawns a background thread to run this method.
87
100
  """
101
+ if self._mq_dao is None:
102
+ self.logger.warning("MQ is disabled; no message listener will run.")
103
+ return
88
104
  self.logger.debug("Going to wait for new messages!")
89
105
  self._mq_dao.message_listener(self.message_handler)
90
106
  self.logger.debug("Broke main message listening loop!")
@@ -96,4 +112,6 @@ class BaseConsumer(object):
96
112
  """
97
113
  Stop consuming messages by unsubscribing from the message queue.
98
114
  """
115
+ if self._mq_dao is None:
116
+ return
99
117
  self._mq_dao.unsubscribe()
@@ -197,6 +197,24 @@ class DocumentInserter(BaseConsumer):
197
197
  f"{'' if exec_bundle_id is None else exec_bundle_id}_{interceptor_instance_id}!"
198
198
  )
199
199
  return "continue"
200
+ elif message["info"] == "mq_flush_complete":
201
+ exec_bundle_id = message.get("exec_bundle_id", None)
202
+ interceptor_instance_id = message.get("interceptor_instance_id")
203
+ self.logger.info(
204
+ f"DocInserter id {id(self)}. Received mq_flush_complete message "
205
+ f"from the interceptor {'' if exec_bundle_id is None else exec_bundle_id}_{interceptor_instance_id}!"
206
+ )
207
+ if self.check_safe_stops:
208
+ self.logger.info(
209
+ f"Begin register_flush_complete "
210
+ f"{'' if exec_bundle_id is None else exec_bundle_id}_{interceptor_instance_id}!"
211
+ )
212
+ self._mq_dao.register_flush_complete(interceptor_instance_id, exec_bundle_id)
213
+ self.logger.info(
214
+ f"Done register_flush_complete "
215
+ f"{'' if exec_bundle_id is None else exec_bundle_id}_{interceptor_instance_id}!"
216
+ )
217
+ return "continue"
200
218
  elif message["info"] == "stop_document_inserter":
201
219
  exec_bundle_id = message.get("exec_bundle_id", None)
202
220
  if self._bundle_exec_id == exec_bundle_id:
@@ -297,7 +315,10 @@ class DocumentInserter(BaseConsumer):
297
315
  return self
298
316
  if self.check_safe_stops:
299
317
  trial = 0
300
- while not self._mq_dao.all_time_based_threads_ended(bundle_exec_id):
318
+ while not (
319
+ self._mq_dao.all_time_based_threads_ended(bundle_exec_id)
320
+ and self._mq_dao.all_flush_complete_received(bundle_exec_id)
321
+ ):
301
322
  self.logger.debug(
302
323
  f"# time_based_threads for bundle_exec_id {bundle_exec_id} is"
303
324
  f"{self._mq_dao._keyvalue_dao.set_count(bundle_exec_id)}"
flowcept/version.py CHANGED
@@ -4,4 +4,4 @@
4
4
  # The expected format is: <Major>.<Minor>.<Patch>
5
5
  # This file is supposed to be automatically modified by the CI Bot.
6
6
  # See .github/workflows/version_bumper.py
7
- __version__ = "0.9.18"
7
+ __version__ = "0.9.19"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowcept
3
- Version: 0.9.18
3
+ Version: 0.9.19
4
4
  Summary: Capture and query workflow provenance data using data observability
5
5
  Author: Oak Ridge National Laboratory
6
6
  License-Expression: MIT
@@ -1,13 +1,13 @@
1
1
  flowcept/__init__.py,sha256=tvVZKyymdqv3qOsgpAyDppBlUiBc0ag4QF21IcS-mVk,2449
2
2
  flowcept/cli.py,sha256=d3hogRpuMwQFIqw_fsYD4y074o3AFslBjlkRvTthOVM,25702
3
- flowcept/configs.py,sha256=JN_YFuNFeFyYe1ieiPhSlEc8PVnU8OXoMcKgf7c46ko,9011
4
- flowcept/version.py,sha256=FVbVkAZsGLJF4VKbw0v8G0N_UTTbGRqm1dWVoX3mjfk,307
3
+ flowcept/configs.py,sha256=hqHdaXbVaXF0IPrUjqPPV_ey5ol31Gx2Gmjh43eQuik,9512
4
+ flowcept/version.py,sha256=cmAij7Yt87_OWquErsjUo2KO0u1npqmoClVqCUwAw90,307
5
5
  flowcept/agents/__init__.py,sha256=8eeD2CiKBtHiDsWdrHK_UreIkKlTq4dUbhHDyzw372o,175
6
- flowcept/agents/agent_client.py,sha256=UiBQkC9WE2weLZR2OTkEOEQt9-zqQOkPwRA17HfI-jk,2027
7
- flowcept/agents/agents_utils.py,sha256=aFJ_RVqE4XlXTG7e6SH6WPqfr7hjT4J3SjSgM6sgY60,8630
6
+ flowcept/agents/agent_client.py,sha256=sfXZfF48pOaznRVgzPbTqkEgFnSrqn7SkgRsNE82-cg,2353
7
+ flowcept/agents/agents_utils.py,sha256=oc_ExBIKYGsPPS-FkZEjuL6RAx75IlYE_ykbIzxHYGg,8375
8
8
  flowcept/agents/dynamic_schema_tracker.py,sha256=TsmXRRkyUkqB-0bEgmeqSms8xj1tMMJeYvjoaO2mtwI,6829
9
- flowcept/agents/flowcept_agent.py,sha256=-PM7sGalZnUk-NVCekfHIY0I7oEZD9mnYGXMxFpo0EM,945
10
- flowcept/agents/flowcept_ctx_manager.py,sha256=OiDzie1qp2ZlSphA-4b5kjUHlzP0yjJ4XcFSiNEGgiU,9717
9
+ flowcept/agents/flowcept_agent.py,sha256=iF9ZucrKU7uz35_W1UdXwXybQPxLpr_MkwSZLU0vAbo,4330
10
+ flowcept/agents/flowcept_ctx_manager.py,sha256=WIA2K5WJ71z-wwvyaHmHir8eIlJUVvuE6UqNrsNLZAk,10275
11
11
  flowcept/agents/gui/__init__.py,sha256=Qw9YKbAzgZqBjMQGnF7XWmfUo0fivtkDISQRK3LA3gU,113
12
12
  flowcept/agents/gui/agent_gui.py,sha256=VpwhQamzFKBfrmibxOIc-8wXtZnd2Cq7tbKahZZOp7c,2995
13
13
  flowcept/agents/gui/audio_utils.py,sha256=piA_dc36io1sYqLF6QArS4AMl-cfDa001jGhYz5LkB4,4279
@@ -36,14 +36,14 @@ flowcept/commons/task_data_preprocess.py,sha256=-ceLexv2ZfZOAYF43DPagGwQPgt_L_lN
36
36
  flowcept/commons/utils.py,sha256=okCShkcuWhzznBtADDDusTdfPXO0W041b2f4Aog-7SE,9831
37
37
  flowcept/commons/vocabulary.py,sha256=0psC4NulNFn88mjTcoT_aT4QxX8ljMFgTOF3FxzM40A,1118
38
38
  flowcept/commons/daos/__init__.py,sha256=RO51svfHOg9naN676zuQwbj_RQ6IFHu-RALeefvtwwk,23
39
- flowcept/commons/daos/keyvalue_dao.py,sha256=g7zgC9hVC1NTllwUAqGt44YqdqYUgAKgPlX8_G4BRGw,3599
39
+ flowcept/commons/daos/keyvalue_dao.py,sha256=_tLnq2Se9deemYmQ9QjGYB_J16ZdAfa2pO3zzd3cQHU,3891
40
40
  flowcept/commons/daos/redis_conn.py,sha256=gFyW-5yf6B8ExEYopCmbap8ki-iEwuIw-KH9f6o7UGQ,1495
41
41
  flowcept/commons/daos/docdb_dao/__init__.py,sha256=qRvXREeUJ4mkhxdC9bzpOsVX6M2FB5hDyLFxhMxTGhs,30
42
42
  flowcept/commons/daos/docdb_dao/docdb_dao_base.py,sha256=YbfSVJPwZGK2GBYkeapRC83HkmP0c6Msv5TriD88RcI,11812
43
43
  flowcept/commons/daos/docdb_dao/lmdb_dao.py,sha256=5FV11hjIpFUZTP4HJwPTswd4TT27EJ3it82TpY5Z2RI,12187
44
44
  flowcept/commons/daos/docdb_dao/mongodb_dao.py,sha256=5x0un15uCDTcnuITOyOhvF9mKj_bUmF2du0AHQfjN9k,40055
45
45
  flowcept/commons/daos/mq_dao/__init__.py,sha256=Xxm4FmbBUZDQ7XIAmSFbeKE_AdHsbgFmSuftvMWSykQ,21
46
- flowcept/commons/daos/mq_dao/mq_dao_base.py,sha256=VXqXzesU01dCHE5i0urnYQppixUNGZbJMRmm4jSAcgM,9424
46
+ flowcept/commons/daos/mq_dao/mq_dao_base.py,sha256=BAegPRL9yozNFJnlkpyc2pW06ZiLXHzGITKFhY2ID7A,10777
47
47
  flowcept/commons/daos/mq_dao/mq_dao_kafka.py,sha256=mWoY9RvViHegzXXynuegU_jg-S55YSV5lfgNqaPuMlg,5085
48
48
  flowcept/commons/daos/mq_dao/mq_dao_mofka.py,sha256=tRdMGYDzdeIJxad-B4-DE6u8Wzs61eTzOW4ojZrnTxs,4057
49
49
  flowcept/commons/daos/mq_dao/mq_dao_redis.py,sha256=be1ejbQofeJhf2qw7AWT9JF8z-a3kk-DbSCMU9wAwuI,6773
@@ -82,11 +82,11 @@ flowcept/flowceptor/adapters/tensorboard/__init__.py,sha256=LrcR4WCIlBwwHIUSteQ8
82
82
  flowcept/flowceptor/adapters/tensorboard/tensorboard_dataclasses.py,sha256=lSfDd6TucVNzGxbm69BYyCVgMr2p9iUEQjnsS4jIfeI,554
83
83
  flowcept/flowceptor/adapters/tensorboard/tensorboard_interceptor.py,sha256=GcRiY93MjuEmdhh37PAyj4ZtwBovA7FPiEM2TjVxsPw,5123
84
84
  flowcept/flowceptor/consumers/__init__.py,sha256=foxtVEb2ZEe9g1slfYIKM4tIFv-He1l7XS--SYs7nlQ,28
85
- flowcept/flowceptor/consumers/base_consumer.py,sha256=hrZ3VFV7pJBMXZsvh7Q2Y36b_ifcnbJkgwe2MiuZL70,3324
85
+ flowcept/flowceptor/consumers/base_consumer.py,sha256=B_oo-FI222PsHErlTiW0IALMPqu2d3cSSRA8nMW5hDk,4113
86
86
  flowcept/flowceptor/consumers/consumer_utils.py,sha256=E6R07zIKNXJTCxvL-OCrCKNYRpqtwRiXiZx0D2BKidk,5893
87
- flowcept/flowceptor/consumers/document_inserter.py,sha256=IeVl6Y4Q1KlpYGvE7uDI0vKQf-MGf2pgnIpxCYtyzKE,13392
87
+ flowcept/flowceptor/consumers/document_inserter.py,sha256=f9EnQfBbMx27UKzCIymNvsyTxuv8mP4ftY4njxvyjo0,14515
88
88
  flowcept/flowceptor/consumers/agent/__init__.py,sha256=R1uvjBPeTLw9SpYgyUc6Qmo16pE84PFHcELTTFvyTWU,56
89
- flowcept/flowceptor/consumers/agent/base_agent_context_manager.py,sha256=sAoMtbfB63Ys7AxUiARlTm9z1QX9HcVNYE-mpjyYo58,4116
89
+ flowcept/flowceptor/consumers/agent/base_agent_context_manager.py,sha256=Hi2fBgyWLks3mamWNoT0dKT5vDspYIURq4eflq-JKeU,4184
90
90
  flowcept/instrumentation/__init__.py,sha256=M5bTmg80E4QyN91gUX3qfw_nbtJSXwGWcKxdZP3vJz0,34
91
91
  flowcept/instrumentation/flowcept_agent_task.py,sha256=XN9JU4LODca0SgojUm4F5iU_V8tuWkOt1fAKcoOAG34,10757
92
92
  flowcept/instrumentation/flowcept_decorator.py,sha256=X4Lp_FSsoL08K8ZhRM4mC0OjKupbQtbMQR8zxy3ezDY,1350
@@ -94,9 +94,9 @@ flowcept/instrumentation/flowcept_loop.py,sha256=nF7Sov-DCDapyYvS8zx-1ZFrnjc3CPg
94
94
  flowcept/instrumentation/flowcept_task.py,sha256=_G1e5SOMQ1y8RhO_rVexe7icQbgFF4TpaHlpW_MxERc,11135
95
95
  flowcept/instrumentation/flowcept_torch.py,sha256=kkZQRYq6cDBpdBU6J39_4oKRVkhyF3ODlz8ydV5WGKw,23455
96
96
  flowcept/instrumentation/task_capture.py,sha256=p_Cj9_cHVMhgzqDXhKqdpk01-88VAAVhbgk5IDa-7sk,8576
97
- resources/sample_settings.yaml,sha256=AYFqPC0im0ia5TiR2Ri_d5C11gLSDaqXX5j5fa79D6Q,6895
98
- flowcept-0.9.18.dist-info/METADATA,sha256=jBMMehf112ABa6qRQhSBU3q-2Aho9KvD02tx5aPEYRU,33386
99
- flowcept-0.9.18.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
100
- flowcept-0.9.18.dist-info/entry_points.txt,sha256=i8q67WE0201rVxYI2lyBtS52shvgl93x2Szp4q8zMlw,47
101
- flowcept-0.9.18.dist-info/licenses/LICENSE,sha256=r5-2P6tFTuRGWT5TiX32s1y0tnp4cIqBEC1QjTaXe2k,1086
102
- flowcept-0.9.18.dist-info/RECORD,,
97
+ resources/sample_settings.yaml,sha256=5k1e81rNmBs7MEW-FyX1DPnrZcQQUrF7imAPKr4Mtys,6895
98
+ flowcept-0.9.19.dist-info/METADATA,sha256=bcyJrlLE1SGz8M96BA6SpVIZkjsCxrgYV6NlYOpxVvM,33386
99
+ flowcept-0.9.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
100
+ flowcept-0.9.19.dist-info/entry_points.txt,sha256=i8q67WE0201rVxYI2lyBtS52shvgl93x2Szp4q8zMlw,47
101
+ flowcept-0.9.19.dist-info/licenses/LICENSE,sha256=r5-2P6tFTuRGWT5TiX32s1y0tnp4cIqBEC1QjTaXe2k,1086
102
+ flowcept-0.9.19.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
- flowcept_version: 0.9.18 # Version of the Flowcept package. This setting file is compatible with this version.
1
+ flowcept_version: 0.9.19 # Version of the Flowcept package. This setting file is compatible with this version.
2
2
 
3
3
  project:
4
4
  debug: true # Toggle debug mode. This will add a property `debug: true` to all saved data, making it easier to retrieve/delete them later.