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.
- flowcept/cli.py +460 -0
- flowcept/commons/daos/keyvalue_dao.py +19 -23
- flowcept/commons/daos/mq_dao/mq_dao_base.py +29 -29
- flowcept/commons/daos/mq_dao/mq_dao_kafka.py +4 -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 +36 -8
- flowcept/commons/settings_factory.py +2 -4
- flowcept/commons/task_data_preprocess.py +200 -0
- flowcept/commons/utils.py +1 -1
- flowcept/configs.py +11 -9
- flowcept/flowcept_api/flowcept_controller.py +30 -13
- flowcept/flowceptor/adapters/agents/__init__.py +1 -0
- flowcept/flowceptor/adapters/agents/agents_utils.py +89 -0
- flowcept/flowceptor/adapters/agents/flowcept_agent.py +292 -0
- flowcept/flowceptor/adapters/agents/flowcept_llm_prov_capture.py +186 -0
- flowcept/flowceptor/adapters/agents/prompts.py +51 -0
- flowcept/flowceptor/adapters/base_interceptor.py +17 -19
- 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 +101 -0
- flowcept/flowceptor/consumers/agent/client_agent.py +48 -0
- flowcept/flowceptor/consumers/agent/flowcept_agent_context_manager.py +145 -0
- flowcept/flowceptor/consumers/agent/flowcept_qa_manager.py +112 -0
- flowcept/flowceptor/consumers/base_consumer.py +90 -0
- flowcept/flowceptor/consumers/document_inserter.py +138 -53
- flowcept/flowceptor/telemetry_capture.py +1 -1
- flowcept/instrumentation/task_capture.py +19 -9
- flowcept/version.py +1 -1
- {flowcept-0.8.9.dist-info → flowcept-0.8.11.dist-info}/METADATA +18 -6
- {flowcept-0.8.9.dist-info → flowcept-0.8.11.dist-info}/RECORD +39 -25
- flowcept-0.8.11.dist-info/entry_points.txt +2 -0
- resources/sample_settings.yaml +44 -23
- 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.9.dist-info → flowcept-0.8.11.dist-info}/WHEEL +0 -0
- {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()
|