flowcept 0.8.9__py3-none-any.whl → 0.8.11__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 (42) hide show
  1. flowcept/cli.py +460 -0
  2. flowcept/commons/daos/keyvalue_dao.py +19 -23
  3. flowcept/commons/daos/mq_dao/mq_dao_base.py +29 -29
  4. flowcept/commons/daos/mq_dao/mq_dao_kafka.py +4 -3
  5. flowcept/commons/daos/mq_dao/mq_dao_mofka.py +4 -0
  6. flowcept/commons/daos/mq_dao/mq_dao_redis.py +38 -5
  7. flowcept/commons/daos/redis_conn.py +47 -0
  8. flowcept/commons/flowcept_dataclasses/task_object.py +36 -8
  9. flowcept/commons/settings_factory.py +2 -4
  10. flowcept/commons/task_data_preprocess.py +200 -0
  11. flowcept/commons/utils.py +1 -1
  12. flowcept/configs.py +11 -9
  13. flowcept/flowcept_api/flowcept_controller.py +30 -13
  14. flowcept/flowceptor/adapters/agents/__init__.py +1 -0
  15. flowcept/flowceptor/adapters/agents/agents_utils.py +89 -0
  16. flowcept/flowceptor/adapters/agents/flowcept_agent.py +292 -0
  17. flowcept/flowceptor/adapters/agents/flowcept_llm_prov_capture.py +186 -0
  18. flowcept/flowceptor/adapters/agents/prompts.py +51 -0
  19. flowcept/flowceptor/adapters/base_interceptor.py +17 -19
  20. flowcept/flowceptor/adapters/brokers/__init__.py +1 -0
  21. flowcept/flowceptor/adapters/brokers/mqtt_interceptor.py +132 -0
  22. flowcept/flowceptor/adapters/mlflow/mlflow_interceptor.py +3 -3
  23. flowcept/flowceptor/adapters/tensorboard/tensorboard_interceptor.py +3 -3
  24. flowcept/flowceptor/consumers/agent/__init__.py +1 -0
  25. flowcept/flowceptor/consumers/agent/base_agent_context_manager.py +101 -0
  26. flowcept/flowceptor/consumers/agent/client_agent.py +48 -0
  27. flowcept/flowceptor/consumers/agent/flowcept_agent_context_manager.py +145 -0
  28. flowcept/flowceptor/consumers/agent/flowcept_qa_manager.py +112 -0
  29. flowcept/flowceptor/consumers/base_consumer.py +90 -0
  30. flowcept/flowceptor/consumers/document_inserter.py +138 -53
  31. flowcept/flowceptor/telemetry_capture.py +1 -1
  32. flowcept/instrumentation/task_capture.py +19 -9
  33. flowcept/version.py +1 -1
  34. {flowcept-0.8.9.dist-info → flowcept-0.8.11.dist-info}/METADATA +18 -6
  35. {flowcept-0.8.9.dist-info → flowcept-0.8.11.dist-info}/RECORD +39 -25
  36. flowcept-0.8.11.dist-info/entry_points.txt +2 -0
  37. resources/sample_settings.yaml +44 -23
  38. flowcept/flowceptor/adapters/zambeze/__init__.py +0 -1
  39. flowcept/flowceptor/adapters/zambeze/zambeze_dataclasses.py +0 -41
  40. flowcept/flowceptor/adapters/zambeze/zambeze_interceptor.py +0 -102
  41. {flowcept-0.8.9.dist-info → flowcept-0.8.11.dist-info}/WHEEL +0 -0
  42. {flowcept-0.8.9.dist-info → flowcept-0.8.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,132 @@
1
+ """Zambeze interceptor module."""
2
+
3
+ import uuid
4
+ from threading import Thread
5
+ from time import sleep
6
+ import paho.mqtt.client as mqtt
7
+ import json
8
+ from typing import Dict
9
+ from flowcept.commons.flowcept_dataclasses.task_object import TaskObject
10
+ from flowcept.flowcept_api.flowcept_controller import Flowcept
11
+ from flowcept.flowceptor.adapters.base_interceptor import (
12
+ BaseInterceptor,
13
+ )
14
+
15
+
16
+ class MQTTBrokerInterceptor(BaseInterceptor):
17
+ """Zambeze interceptor."""
18
+
19
+ def __init__(self, plugin_key="broker_mqtt"):
20
+ super().__init__(plugin_key)
21
+
22
+ assert self.settings.get("protocol") == "mqtt3.1.1", "We only support mqtt3.1.1 for this interceptor."
23
+ self._host = self.settings.get("host", "localhost")
24
+ self._port = self.settings.get("port", 1883)
25
+ self._username = self.settings.get("username", "username")
26
+ self._password = self.settings.get("password", None)
27
+ self._queues = self.settings.get("queues")
28
+ self._qos = self.settings.get("qos", 2)
29
+ self._id = str(id(self))
30
+
31
+ self._tracked_keys = self.settings.get("tracked_keys")
32
+ self._task_subtype = self.settings.get("task_subtype", None)
33
+ self._client: mqtt.Client = None
34
+
35
+ self._observer_thread: Thread = None
36
+
37
+ def _connect(self):
38
+ """Establish a connection to the MQTT broker."""
39
+ try:
40
+ self._client = mqtt.Client(client_id=self._id, clean_session=False, protocol=mqtt.MQTTv311)
41
+ self._client.username_pw_set(self._username, self._password)
42
+
43
+ self._client.on_message = self.callback
44
+ self._client.on_connect = self._on_connect
45
+ self._client.on_disconnect = self._on_disconnect
46
+
47
+ self.logger.debug("Connecting to MQTT broker...")
48
+ self._client.connect(self._host, self._port, 60)
49
+ self.logger.debug("Connected.")
50
+ except Exception as e:
51
+ self.logger.error(f"Connection failed: {e}")
52
+ raise e
53
+
54
+ def _on_connect(self, *_):
55
+ """Handle connection events and subscribe to the topic."""
56
+ for q in self._queues:
57
+ self.logger.debug(f"Client {self._id} connected to MQTT queue {q}. Waiting for messages...")
58
+ self._client.subscribe(q, qos=self._qos)
59
+
60
+ def callback(self, _, __, msg):
61
+ """Implement the callback."""
62
+ msg_str = msg.payload.decode()
63
+ topic = msg.topic
64
+ self.logger.debug(f"Received message: '{msg_str}' on topic '{topic}'")
65
+
66
+ msg_dict = json.loads(msg_str)
67
+ msg_dict["topic"] = topic
68
+
69
+ task_msg = self.prepare_task_msg(msg_dict)
70
+ self.intercept(task_msg.to_dict())
71
+
72
+ def _on_disconnect(self, *_):
73
+ """Handle disconnections and attempt reconnection."""
74
+ self.logger.warning("MQTT Observer Client Disconnected.")
75
+
76
+ def start(self, bundle_exec_id) -> "MQTTBrokerInterceptor":
77
+ """Start it."""
78
+ super().start(bundle_exec_id)
79
+ self._observer_thread = Thread(target=self.observe, daemon=True)
80
+ self._observer_thread.start()
81
+ return self
82
+
83
+ def observe(self):
84
+ """Start the MQTT loop."""
85
+ self._connect()
86
+ self._client.loop_forever()
87
+
88
+ def prepare_task_msg(self, msg: Dict) -> TaskObject:
89
+ """Prepare a task."""
90
+ task_dict = {}
91
+ custom_metadata = {"topic": msg.get("topic", None)}
92
+ for key in self._tracked_keys:
93
+ if key != "custom_metadata":
94
+ if self._tracked_keys.get(key):
95
+ task_dict[key] = msg.get(self._tracked_keys.get(key), None)
96
+ else:
97
+ cm = self._tracked_keys.get("custom_metadata", None)
98
+ if cm and len(cm):
99
+ for k in cm:
100
+ custom_metadata[k] = msg[k]
101
+ task_dict["custom_metadata"] = custom_metadata
102
+
103
+ if isinstance(task_dict.get("used"), str):
104
+ task_dict["used"] = {"payload": task_dict.get("used")}
105
+
106
+ if "task_id" not in task_dict:
107
+ task_dict["task_id"] = str(uuid.uuid4())
108
+
109
+ task_obj = TaskObject.from_dict(task_dict)
110
+ task_obj.subtype = self._task_subtype
111
+
112
+ if task_obj.campaign_id is None:
113
+ task_obj.campaign_id = Flowcept.campaign_id
114
+
115
+ if task_obj.workflow_id is None:
116
+ task_obj.workflow_id = Flowcept.current_workflow_id
117
+
118
+ print(task_obj)
119
+ return task_obj
120
+
121
+ def stop(self) -> bool:
122
+ """Stop it."""
123
+ self.logger.debug("Interceptor stopping...")
124
+ super().stop()
125
+ try:
126
+ self._client.disconnect()
127
+ except Exception as e:
128
+ self.logger.warning(f"This exception is expected to occur after channel.basic_cancel: {e}")
129
+ sleep(2)
130
+ self._observer_thread.join()
131
+ self.logger.debug("Interceptor stopped.")
132
+ return True
@@ -64,17 +64,17 @@ class MLFlowInterceptor(BaseInterceptor):
64
64
  task_msg = self.prepare_task_msg(run_data).to_dict()
65
65
  self.intercept(task_msg)
66
66
 
67
- def start(self, bundle_exec_id) -> "MLFlowInterceptor":
67
+ def start(self, bundle_exec_id, check_safe_stops) -> "MLFlowInterceptor":
68
68
  """Start it."""
69
69
  super().start(bundle_exec_id)
70
70
  self._observer_thread = Thread(target=self.observe, daemon=True)
71
71
  self._observer_thread.start()
72
72
  return self
73
73
 
74
- def stop(self) -> bool:
74
+ def stop(self, check_safe_stops: bool = True) -> bool:
75
75
  """Stop it."""
76
76
  sleep(1)
77
- super().stop()
77
+ super().stop(check_safe_stops)
78
78
  self.logger.debug("Interceptor stopping...")
79
79
  self._observer.stop()
80
80
  self._observer_thread.join()
@@ -91,17 +91,17 @@ class TensorboardInterceptor(BaseInterceptor):
91
91
  self.intercept(task_msg.to_dict())
92
92
  self.state_manager.add_element_id(child_event.log_path)
93
93
 
94
- def start(self, bundle_exec_id) -> "TensorboardInterceptor":
94
+ def start(self, bundle_exec_id, check_safe_stops: bool = True) -> "TensorboardInterceptor":
95
95
  """Start it."""
96
96
  super().start(bundle_exec_id)
97
97
  self.observe()
98
98
  return self
99
99
 
100
- def stop(self) -> bool:
100
+ def stop(self, check_safe_stops: bool = True) -> bool:
101
101
  """Stop it."""
102
102
  sleep(1)
103
103
  self.logger.debug("Interceptor stopping...")
104
- super().stop()
104
+ super().stop(check_safe_stops)
105
105
  self._observer.stop()
106
106
  self.logger.debug("Interceptor stopped.")
107
107
  return True
@@ -0,0 +1 @@
1
+ """Flowcept agent and Flowcept-enabled agent module."""
@@ -0,0 +1,101 @@
1
+ from contextlib import asynccontextmanager
2
+ from dataclasses import dataclass
3
+ from typing import Dict, List
4
+
5
+ from flowcept.flowceptor.consumers.base_consumer import BaseConsumer
6
+
7
+
8
+ @dataclass
9
+ class BaseAppContext:
10
+ """
11
+ Container for storing agent context data during the lifespan of an application session.
12
+
13
+ Attributes
14
+ ----------
15
+ tasks : list of dict
16
+ A list of task messages received from the message queue. Each task message is stored as a dictionary.
17
+ """
18
+
19
+ tasks: List[Dict]
20
+
21
+
22
+ class BaseAgentContextManager(BaseConsumer):
23
+ """
24
+ Base class for any MCP Agent that wants to participate in the Flowcept ecosystem.
25
+
26
+ Agents inheriting from this class can:
27
+ - Subscribe to and consume messages from the Flowcept-compatible message queue (MQ)
28
+ - Handle task-related messages and accumulate them in context
29
+ - Gracefully manage their lifecycle using an async context manager
30
+ - Interact with Flowcept’s provenance system to read/write messages, query databases, and store chat history
31
+
32
+ To integrate with Flowcept:
33
+ - Inherit from `BaseAgentContextManager`
34
+ - Override `message_handler()` if custom message handling is needed
35
+ - Access shared state via `self.context` during execution
36
+ """
37
+
38
+ def __init__(self):
39
+ """
40
+ Initializes the agent and resets its context state.
41
+ """
42
+ super().__init__()
43
+ self.context = None
44
+ self.reset_context()
45
+
46
+ def message_handler(self, msg_obj: Dict) -> bool:
47
+ """
48
+ Handles a single message received from the message queue.
49
+
50
+ Parameters
51
+ ----------
52
+ msg_obj : dict
53
+ The message received, typically structured with a "type" field.
54
+
55
+ Returns
56
+ -------
57
+ bool
58
+ Return True to continue listening for messages, or False to stop the loop.
59
+
60
+ Notes
61
+ -----
62
+ This default implementation stores messages of type 'task' in the internal context.
63
+ Override this method in a subclass to handle other message types or implement custom logic.
64
+ """
65
+ msg_type = msg_obj.get("type", None)
66
+ msg_subtype = msg_obj.get("subtype", "")
67
+ if msg_type == "task":
68
+ self.logger.debug("Received task msg!")
69
+ if msg_subtype not in {"llm_query"}:
70
+ self.context.tasks.append(msg_obj)
71
+
72
+ return True
73
+
74
+ def reset_context(self):
75
+ """
76
+ Resets the internal context, clearing all stored task data.
77
+ """
78
+ self.context = BaseAppContext(tasks=[])
79
+
80
+ @asynccontextmanager
81
+ async def lifespan(self, app):
82
+ """
83
+ Async context manager to handle the agent’s lifecycle within an application.
84
+
85
+ Starts the message consumption when the context is entered and stops it when exited.
86
+
87
+ Parameters
88
+ ----------
89
+ app : Any
90
+ The application instance using this context (typically unused but included for compatibility).
91
+
92
+ Yields
93
+ ------
94
+ BaseAppContext
95
+ The current application context, including collected tasks.
96
+ """
97
+ self.start()
98
+ try:
99
+ yield self.context
100
+ finally:
101
+ self.stop_consumption()
@@ -0,0 +1,48 @@
1
+ from typing import Dict, List
2
+
3
+ from flowcept.configs import AGENT
4
+ from mcp import ClientSession
5
+ from mcp.client.streamable_http import streamablehttp_client
6
+ from mcp.types import TextContent
7
+
8
+ MCP_HOST = AGENT.get("mcp_host", "0.0.0.0")
9
+ MCP_PORT = AGENT.get("mcp_port", 8000)
10
+
11
+ MCP_URL = f"http://{MCP_HOST}:{MCP_PORT}/mcp"
12
+
13
+
14
+ def run_tool(tool_name: str, kwargs: Dict = None) -> List[TextContent]:
15
+ """
16
+ Run a tool using an MCP client session via a local streamable HTTP connection.
17
+
18
+ This function opens an asynchronous connection to a local MCP server,
19
+ initializes a session, and invokes a specified tool with optional arguments.
20
+ The tool's response content is returned as a list of `TextContent` objects.
21
+
22
+ Parameters
23
+ ----------
24
+ tool_name : str
25
+ The name of the tool to call within the MCP framework.
26
+ kwargs : Dict, optional
27
+ A dictionary of keyword arguments to pass as input to the tool. Defaults to None.
28
+
29
+ Returns
30
+ -------
31
+ List[TextContent]
32
+ A list of `TextContent` objects returned by the tool execution.
33
+
34
+ Notes
35
+ -----
36
+ This function uses `asyncio.run`, so it must not be called from an already-running
37
+ event loop (e.g., inside another async function in environments like Jupyter).
38
+ """
39
+ import asyncio
40
+
41
+ async def _run():
42
+ async with streamablehttp_client(MCP_URL) as (read, write, _):
43
+ async with ClientSession(read, write) as session:
44
+ await session.initialize()
45
+ result = await session.call_tool(tool_name, arguments=kwargs)
46
+ return result.content
47
+
48
+ return asyncio.run(_run())
@@ -0,0 +1,145 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, List
3
+
4
+ from flowcept.flowceptor.consumers.agent.base_agent_context_manager import BaseAgentContextManager, BaseAppContext
5
+ from langchain.chains.retrieval_qa.base import BaseRetrievalQA
6
+ from langchain_community.embeddings import HuggingFaceEmbeddings
7
+
8
+ from flowcept.flowceptor.consumers.agent import client_agent
9
+ from flowcept.flowceptor.consumers.agent.flowcept_qa_manager import FlowceptQAManager
10
+ from flowcept.commons.task_data_preprocess import summarize_task
11
+
12
+
13
+ @dataclass
14
+ class FlowceptAppContext(BaseAppContext):
15
+ """
16
+ Context object for holding flowcept-specific state (e.g., tasks data) during the agent's lifecycle.
17
+
18
+ Attributes
19
+ ----------
20
+ task_summaries : List[Dict]
21
+ List of summarized task dictionaries.
22
+ critical_tasks : List[Dict]
23
+ List of critical task summaries with tags or anomalies.
24
+ qa_chain : BaseRetrievalQA
25
+ The QA chain used for question-answering over task summaries.
26
+ vectorstore_path : str
27
+ Path to the persisted vectorstore used by the QA system.
28
+ embedding_model : HuggingFaceEmbeddings
29
+ The embedding model used to generate vector representations for tasks.
30
+ """
31
+
32
+ task_summaries: List[Dict]
33
+ critical_tasks: List[Dict]
34
+ qa_chain: BaseRetrievalQA
35
+ vectorstore_path: str
36
+ embedding_model: HuggingFaceEmbeddings
37
+
38
+
39
+ class FlowceptAgentContextManager(BaseAgentContextManager):
40
+ """
41
+ Manages agent context and operations for Flowcept's intelligent task monitoring.
42
+
43
+ This class extends BaseAgentContextManager and maintains a rolling buffer of task messages.
44
+ It summarizes and tags tasks, builds a QA index over them, and uses LLM tools to analyze
45
+ task batches periodically.
46
+
47
+ Attributes
48
+ ----------
49
+ context : FlowceptAppContext
50
+ Current application context holding task state and QA components.
51
+ msgs_counter : int
52
+ Counter tracking how many task messages have been processed.
53
+ context_size : int
54
+ Number of task messages to collect before triggering QA index building and LLM analysis.
55
+ qa_manager : FlowceptQAManager
56
+ Utility for constructing QA chains from task summaries.
57
+ """
58
+
59
+ def __init__(self):
60
+ super().__init__()
61
+ self.context: FlowceptAppContext = None
62
+ self.reset_context()
63
+ self.msgs_counter = 0
64
+ self.context_size = 5
65
+ self.qa_manager = FlowceptQAManager()
66
+
67
+ def message_handler(self, msg_obj: Dict):
68
+ """
69
+ Handle an incoming message and update context accordingly.
70
+
71
+ Parameters
72
+ ----------
73
+ msg_obj : Dict
74
+ The incoming message object.
75
+
76
+ Returns
77
+ -------
78
+ bool
79
+ True if the message was handled successfully.
80
+ """
81
+ msg_type = msg_obj.get("type", None)
82
+ if msg_type == "task":
83
+ self.msgs_counter += 1
84
+ self.logger.debug("Received task msg!")
85
+ self.context.tasks.append(msg_obj)
86
+
87
+ self.logger.debug(f"This is QA! {self.context.qa_chain}")
88
+
89
+ task_summary = summarize_task(msg_obj)
90
+ self.context.task_summaries.append(task_summary)
91
+ if len(task_summary.get("tags", [])):
92
+ self.context.critical_tasks.append(task_summary)
93
+
94
+ if self.msgs_counter > 0 and self.msgs_counter % self.context_size == 0:
95
+ self.build_qa_index()
96
+
97
+ self.monitor_chunk()
98
+
99
+ return True
100
+
101
+ def monitor_chunk(self):
102
+ """
103
+ Perform LLM-based analysis on the current chunk of task messages and send the results.
104
+ """
105
+ self.logger.debug(f"Going to begin LLM job! {self.msgs_counter}")
106
+ result = client_agent.run_tool("analyze_task_chunk")
107
+ if len(result):
108
+ content = result[0].text
109
+ if content != "Error executing tool":
110
+ msg = {"type": "flowcept_agent", "info": "monitor", "content": content}
111
+ self._mq_dao.send_message(msg)
112
+ self.logger.debug(str(content))
113
+ else:
114
+ self.logger.error(content)
115
+
116
+ def build_qa_index(self):
117
+ """
118
+ Build a new QA index from the current list of task summaries.
119
+ """
120
+ self.logger.debug(f"Going to begin QA Build! {self.msgs_counter}")
121
+ try:
122
+ qa_chain_result = self.qa_manager.build_qa(docs=self.context.task_summaries)
123
+
124
+ self.context.qa_chain = qa_chain_result.get("qa_chain")
125
+ self.context.vectorstore_path = qa_chain_result.get("path")
126
+
127
+ self.logger.debug(f"Built QA! {self.msgs_counter}")
128
+ assert self.context.qa_chain is not None
129
+ self.logger.debug(f"This is QA! {self.context.qa_chain}")
130
+ self.logger.debug(f"This is QA path! {self.context.vectorstore_path}")
131
+ except Exception as e:
132
+ self.logger.exception(e)
133
+
134
+ def reset_context(self):
135
+ """
136
+ Reset the agent's context to a clean state, initializing a new QA setup.
137
+ """
138
+ self.context = FlowceptAppContext(
139
+ tasks=[],
140
+ task_summaries=[],
141
+ critical_tasks=[],
142
+ qa_chain=None,
143
+ vectorstore_path=None,
144
+ embedding_model=FlowceptQAManager.embedding_model,
145
+ )
@@ -0,0 +1,112 @@
1
+ from typing import Dict, List
2
+
3
+ from langchain.chains.retrieval_qa.base import RetrievalQA
4
+ from langchain_community.embeddings import HuggingFaceEmbeddings
5
+ from langchain_community.vectorstores import FAISS
6
+ from langchain.schema import Document
7
+ from langchain_core.language_models import LLM
8
+
9
+ from flowcept.commons.flowcept_logger import FlowceptLogger
10
+ from flowcept.flowceptor.adapters.agents.agents_utils import build_llm_model
11
+
12
+
13
+ # TODO If all methods are static, this doesnt need to be a class.
14
+ class FlowceptQAManager(object):
15
+ """
16
+ Manager for building and loading question-answering (QA) chains using LangChain.
17
+
18
+ This utility constructs a `RetrievalQA` chain by converting task dictionaries into
19
+ `Document` objects, embedding them with HuggingFace, storing them in a FAISS vectorstore,
20
+ and returning a ready-to-query QA pipeline.
21
+
22
+ Attributes
23
+ ----------
24
+ embedding_model : HuggingFaceEmbeddings
25
+ The default embedding model used to embed documents into vector representations.
26
+ """
27
+
28
+ embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
29
+
30
+ @staticmethod
31
+ def build_qa(docs: List[Dict] = None, llm: LLM = None):
32
+ """
33
+ Build a RetrievalQA chain from a list of task dictionaries.
34
+
35
+ Parameters
36
+ ----------
37
+ docs : List[Dict], optional
38
+ A list of task dictionaries to be converted into retrievable documents.
39
+ llm : LLM, optional
40
+ The language model to use for the QA chain. If None, a default model is built.
41
+
42
+ Returns
43
+ -------
44
+ dict
45
+ A dictionary containing:
46
+ - 'qa_chain': the constructed RetrievalQA chain
47
+ - 'path': local path where the FAISS vectorstore is saved
48
+
49
+ Notes
50
+ -----
51
+ If no documents are provided, the method returns None.
52
+ """
53
+ if not len(docs):
54
+ return None
55
+
56
+ if llm is None:
57
+ llm = build_llm_model()
58
+
59
+ documents = []
60
+ for d in docs:
61
+ content = str(d) # convert the dict to a string
62
+ metadata = {"task_id": d.get("task_id", "unknown")}
63
+ documents.append(Document(page_content=content, metadata=metadata))
64
+
65
+ FlowceptLogger().debug(f"Number of documents to index: {len(documents)}")
66
+ vectorstore = FAISS.from_documents(documents=documents, embedding=FlowceptQAManager.embedding_model)
67
+ path = "/tmp/qa_index"
68
+ vectorstore.save_local(path)
69
+
70
+ retriever = vectorstore.as_retriever()
71
+ qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=retriever, return_source_documents=True)
72
+
73
+ return {"qa_chain": qa_chain, "path": path}
74
+
75
+ @staticmethod
76
+ def _load_qa_chain(path, llm=None, embedding_model=None) -> RetrievalQA:
77
+ if embedding_model is None:
78
+ embedding_model = FlowceptQAManager.embedding_model
79
+ if llm is None:
80
+ llm = build_llm_model()
81
+
82
+ vectorstore = FAISS.load_local(path, embeddings=embedding_model, allow_dangerous_deserialization=True)
83
+
84
+ retriever = vectorstore.as_retriever()
85
+
86
+ return RetrievalQA.from_chain_type(llm=llm, retriever=retriever, return_source_documents=True)
87
+
88
+ @staticmethod
89
+ def build_qa_chain_from_vectorstore_path(vectorstore_path, llm=None) -> RetrievalQA:
90
+ """
91
+ Build a RetrievalQA chain from an existing vectorstore path.
92
+
93
+ Parameters
94
+ ----------
95
+ vectorstore_path : str
96
+ Path to the FAISS vectorstore previously saved to disk.
97
+ llm : LLM, optional
98
+ Language model to use. If None, a default model is built.
99
+
100
+ Returns
101
+ -------
102
+ RetrievalQA
103
+ A RetrievalQA chain constructed using the loaded vectorstore.
104
+ """
105
+ if llm is None:
106
+ llm = build_llm_model() # TODO: consider making this llm instance static
107
+ qa_chain = FlowceptQAManager._load_qa_chain(
108
+ path=vectorstore_path, # Only here we really need the QA. We might no
109
+ llm=llm,
110
+ embedding_model=FlowceptQAManager.embedding_model,
111
+ )
112
+ return qa_chain
@@ -0,0 +1,90 @@
1
+ from abc import abstractmethod
2
+ from threading import Thread
3
+ from typing import Callable, Dict, Tuple, Optional
4
+
5
+ from flowcept.commons.daos.mq_dao.mq_dao_base import MQDao
6
+ from flowcept.commons.flowcept_logger import FlowceptLogger
7
+
8
+
9
+ class BaseConsumer(object):
10
+ """
11
+ Abstract base class for message consumers in a pub-sub architecture.
12
+
13
+ This class provides a standard interface and shared logic for subscribing to
14
+ message queues and dispatching messages to a handler.
15
+ """
16
+
17
+ def __init__(self):
18
+ """Initialize the message queue DAO and logger."""
19
+ self._mq_dao = MQDao.build()
20
+ self.logger = FlowceptLogger()
21
+ self._main_thread: Optional[Thread] = None
22
+
23
+ @abstractmethod
24
+ def message_handler(self, msg_obj: Dict) -> bool:
25
+ """
26
+ Handle a single incoming message.
27
+
28
+ Parameters
29
+ ----------
30
+ msg_obj : dict
31
+ The parsed message object received from the queue.
32
+
33
+ Returns
34
+ -------
35
+ bool
36
+ Return False to break the message listener loop.
37
+ Return True to continue listening.
38
+ """
39
+ pass
40
+
41
+ def start(self, target: Callable = None, args: Tuple = (), threaded: bool = True, daemon=False):
42
+ """
43
+ Start the consumer by subscribing and launching the message handler.
44
+
45
+ Parameters
46
+ ----------
47
+ target : Callable
48
+ The function to run for listening to messages (usually the message loop).
49
+ args : tuple, optional
50
+ Arguments to pass to the target function.
51
+ threaded : bool, default=True
52
+ Whether to run the target function in a background thread.
53
+ daemon: bool
54
+
55
+ Returns
56
+ -------
57
+ BaseConsumer
58
+ The current instance (to allow chaining).
59
+ """
60
+ if target is None:
61
+ target = self.default_thread_target
62
+ self._mq_dao.subscribe()
63
+ if threaded:
64
+ self._main_thread = Thread(target=target, args=args, daemon=daemon)
65
+ self._main_thread.start()
66
+ else:
67
+ target(*args)
68
+ return self
69
+
70
+ def default_thread_target(self):
71
+ """
72
+ The default message consumption loop.
73
+
74
+ This method is used as the default thread target when starting the consumer. It listens for
75
+ messages from the message queue and passes them to the consumer's `message_handler`.
76
+
77
+ Typically run in a background thread when `start()` is called without a custom target.
78
+
79
+ See Also
80
+ --------
81
+ start : Starts the consumer and optionally spawns a background thread to run this method.
82
+ """
83
+ self.logger.debug("Going to wait for new messages!")
84
+ self._mq_dao.message_listener(self.message_handler)
85
+
86
+ def stop_consumption(self):
87
+ """
88
+ Stop consuming messages by unsubscribing from the message queue.
89
+ """
90
+ self._mq_dao.unsubscribe()