flowcept 0.8.11__py3-none-any.whl → 0.9.1__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/{flowceptor/consumers/agent/client_agent.py → agents/agent_client.py} +22 -12
- 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/{flowceptor/adapters/agents/prompts.py → agents/prompts/general_prompts.py} +18 -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 +286 -44
- flowcept/commons/daos/docdb_dao/mongodb_dao.py +47 -0
- flowcept/commons/daos/mq_dao/mq_dao_base.py +24 -13
- flowcept/commons/daos/mq_dao/mq_dao_kafka.py +18 -2
- flowcept/commons/flowcept_dataclasses/task_object.py +16 -21
- flowcept/commons/flowcept_dataclasses/workflow_object.py +9 -1
- flowcept/commons/task_data_preprocess.py +260 -60
- flowcept/commons/utils.py +25 -6
- flowcept/configs.py +41 -26
- flowcept/flowcept_api/flowcept_controller.py +73 -6
- flowcept/flowceptor/adapters/base_interceptor.py +11 -5
- flowcept/flowceptor/consumers/agent/base_agent_context_manager.py +25 -1
- flowcept/flowceptor/consumers/base_consumer.py +4 -0
- flowcept/flowceptor/consumers/consumer_utils.py +5 -4
- flowcept/flowceptor/consumers/document_inserter.py +2 -2
- flowcept/flowceptor/telemetry_capture.py +5 -2
- 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 +83 -6
- flowcept/version.py +1 -1
- {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/METADATA +42 -14
- {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/RECORD +50 -36
- resources/sample_settings.yaml +12 -4
- flowcept/flowceptor/adapters/agents/__init__.py +0 -1
- flowcept/flowceptor/adapters/agents/agents_utils.py +0 -89
- flowcept/flowceptor/adapters/agents/flowcept_agent.py +0 -292
- flowcept/flowceptor/adapters/agents/flowcept_llm_prov_capture.py +0 -186
- flowcept/flowceptor/consumers/agent/flowcept_agent_context_manager.py +0 -145
- flowcept/flowceptor/consumers/agent/flowcept_qa_manager.py +0 -112
- {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/WHEEL +0 -0
- {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/entry_points.txt +0 -0
- {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/licenses/LICENSE +0 -0
flowcept/__init__.py
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from flowcept.version import __version__
|
|
4
4
|
|
|
5
|
-
from flowcept.commons.flowcept_dataclasses.workflow_object import (
|
|
6
|
-
WorkflowObject,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
5
|
|
|
10
6
|
def __getattr__(name):
|
|
11
7
|
if name == "Flowcept":
|
|
@@ -13,6 +9,13 @@ def __getattr__(name):
|
|
|
13
9
|
|
|
14
10
|
return Flowcept
|
|
15
11
|
|
|
12
|
+
elif name == "WorkflowObject":
|
|
13
|
+
from flowcept.commons.flowcept_dataclasses.workflow_object import (
|
|
14
|
+
WorkflowObject,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
return WorkflowObject
|
|
18
|
+
|
|
16
19
|
elif name == "flowcept_task":
|
|
17
20
|
from flowcept.instrumentation.flowcept_task import flowcept_task
|
|
18
21
|
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Dict, List, Callable
|
|
2
3
|
|
|
3
|
-
from flowcept.configs import
|
|
4
|
+
from flowcept.configs import AGENT_HOST, AGENT_PORT
|
|
4
5
|
from mcp import ClientSession
|
|
5
6
|
from mcp.client.streamable_http import streamablehttp_client
|
|
6
7
|
from mcp.types import TextContent
|
|
7
8
|
|
|
8
|
-
MCP_HOST = AGENT.get("mcp_host", "0.0.0.0")
|
|
9
|
-
MCP_PORT = AGENT.get("mcp_port", 8000)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def run_tool(tool_name: str, kwargs: Dict = None) -> List[TextContent]:
|
|
10
|
+
def run_tool(
|
|
11
|
+
tool_name: str | Callable, kwargs: Dict = None, host: str = AGENT_HOST, port: int = AGENT_PORT
|
|
12
|
+
) -> List[str]:
|
|
15
13
|
"""
|
|
16
14
|
Run a tool using an MCP client session via a local streamable HTTP connection.
|
|
17
15
|
|
|
@@ -36,13 +34,25 @@ def run_tool(tool_name: str, kwargs: Dict = None) -> List[TextContent]:
|
|
|
36
34
|
This function uses `asyncio.run`, so it must not be called from an already-running
|
|
37
35
|
event loop (e.g., inside another async function in environments like Jupyter).
|
|
38
36
|
"""
|
|
39
|
-
|
|
37
|
+
if isinstance(tool_name, Callable):
|
|
38
|
+
tool_name = tool_name.__name__
|
|
40
39
|
|
|
41
40
|
async def _run():
|
|
42
|
-
|
|
41
|
+
mcp_url = f"http://{host}:{port}/mcp"
|
|
42
|
+
print(mcp_url)
|
|
43
|
+
print(tool_name)
|
|
44
|
+
|
|
45
|
+
async with streamablehttp_client(mcp_url) as (read, write, _):
|
|
43
46
|
async with ClientSession(read, write) as session:
|
|
44
47
|
await session.initialize()
|
|
45
|
-
result = await session.call_tool(tool_name, arguments=kwargs)
|
|
46
|
-
|
|
48
|
+
result: List[TextContent] = await session.call_tool(tool_name, arguments=kwargs)
|
|
49
|
+
actual_result = []
|
|
50
|
+
for r in result.content:
|
|
51
|
+
if isinstance(r, str):
|
|
52
|
+
actual_result.append(r)
|
|
53
|
+
else:
|
|
54
|
+
actual_result.append(r.text)
|
|
55
|
+
|
|
56
|
+
return actual_result
|
|
47
57
|
|
|
48
58
|
return asyncio.run(_run())
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Union, Dict
|
|
3
|
+
|
|
4
|
+
from flowcept.flowceptor.consumers.agent.base_agent_context_manager import BaseAgentContextManager
|
|
5
|
+
from flowcept.instrumentation.flowcept_agent_task import FlowceptLLM, get_current_context_task
|
|
6
|
+
|
|
7
|
+
from flowcept.configs import AGENT
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToolResult(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
ToolResult is a standardized wrapper for tool outputs, encapsulating
|
|
14
|
+
status codes, results, and optional metadata.
|
|
15
|
+
|
|
16
|
+
This class provides conventions for interpreting the output of tools
|
|
17
|
+
(e.g., LLM calls, DataFrame operations, plotting functions) and ensures
|
|
18
|
+
consistent handling of both successes and errors.
|
|
19
|
+
|
|
20
|
+
Conventions
|
|
21
|
+
-----------
|
|
22
|
+
- **2xx: Success (string result)**
|
|
23
|
+
- Result is the expected output as a string.
|
|
24
|
+
- Example: ``201`` → operation completed successfully.
|
|
25
|
+
|
|
26
|
+
- **3xx: Success (dict result)**
|
|
27
|
+
- Result is the expected output as a dictionary.
|
|
28
|
+
- Example: ``301`` → operation completed successfully.
|
|
29
|
+
|
|
30
|
+
- **4xx: Error (string message)**
|
|
31
|
+
- System or agent internal error, returned as a string message.
|
|
32
|
+
- ``400``: LLM call problem (e.g., server connection or token issues).
|
|
33
|
+
- ``404``: Empty or ``None`` result.
|
|
34
|
+
- ``405``: LLM responded, but format was wrong.
|
|
35
|
+
- ``406``: Error executing Python code.
|
|
36
|
+
- ``499``: Other uncategorized error.
|
|
37
|
+
|
|
38
|
+
- **5xx: Error (dict result)**
|
|
39
|
+
- System or agent internal error, returned as a structured dictionary.
|
|
40
|
+
|
|
41
|
+
- **None**
|
|
42
|
+
- Result not yet set or tool did not return anything.
|
|
43
|
+
|
|
44
|
+
Attributes
|
|
45
|
+
----------
|
|
46
|
+
code : int or None
|
|
47
|
+
Status code indicating success or error category.
|
|
48
|
+
result : str or dict, optional
|
|
49
|
+
The main output of the tool (string, dict, or error message).
|
|
50
|
+
extra : dict or str or None
|
|
51
|
+
Additional metadata or debugging information.
|
|
52
|
+
tool_name : str or None
|
|
53
|
+
Name of the tool that produced this result.
|
|
54
|
+
|
|
55
|
+
Methods
|
|
56
|
+
-------
|
|
57
|
+
result_is_str() -> bool
|
|
58
|
+
Return True if the result should be interpreted as a string.
|
|
59
|
+
is_success() -> bool
|
|
60
|
+
Return True if the result represents any type of success.
|
|
61
|
+
is_success_string() -> bool
|
|
62
|
+
Return True if the result is a success with a string output (2xx).
|
|
63
|
+
is_error_string() -> bool
|
|
64
|
+
Return True if the result is an error with a string message (4xx).
|
|
65
|
+
is_success_dict() -> bool
|
|
66
|
+
Return True if the result is a success with a dict output (3xx).
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> ToolResult(code=201, result="Operation successful")
|
|
71
|
+
ToolResult(code=201, result='Operation successful')
|
|
72
|
+
|
|
73
|
+
>>> ToolResult(code=301, result={"data": [1, 2, 3]})
|
|
74
|
+
ToolResult(code=301, result={'data': [1, 2, 3]})
|
|
75
|
+
|
|
76
|
+
>>> ToolResult(code=405, result="Invalid format from LLM")
|
|
77
|
+
ToolResult(code=405, result='Invalid format from LLM')
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
code: int | None = None
|
|
81
|
+
result: Union[str, Dict] = None
|
|
82
|
+
extra: Dict | str | None = None
|
|
83
|
+
tool_name: str | None = None
|
|
84
|
+
|
|
85
|
+
def result_is_str(self) -> bool:
|
|
86
|
+
"""Returns True if the result is a string."""
|
|
87
|
+
return (200 <= self.code < 300) or (400 <= self.code < 500)
|
|
88
|
+
|
|
89
|
+
def is_success(self):
|
|
90
|
+
"""Returns True if the result is a success."""
|
|
91
|
+
return self.is_success_string() or self.is_success_dict()
|
|
92
|
+
|
|
93
|
+
def is_success_string(self):
|
|
94
|
+
"""Returns True if the result is a success string."""
|
|
95
|
+
return 200 <= self.code < 300
|
|
96
|
+
|
|
97
|
+
def is_error_string(self):
|
|
98
|
+
"""Returns True if the result is an error string."""
|
|
99
|
+
return 400 <= self.code < 500
|
|
100
|
+
|
|
101
|
+
def is_success_dict(self) -> bool:
|
|
102
|
+
"""Returns True if the result is a success dictionary."""
|
|
103
|
+
return 300 <= self.code < 400
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_llm_model(
|
|
107
|
+
model_name=None,
|
|
108
|
+
model_kwargs=None,
|
|
109
|
+
service_provider=None,
|
|
110
|
+
agent_id=BaseAgentContextManager.agent_id,
|
|
111
|
+
track_tools=True,
|
|
112
|
+
) -> FlowceptLLM:
|
|
113
|
+
"""
|
|
114
|
+
Build and return an LLM instance using agent configuration.
|
|
115
|
+
|
|
116
|
+
This function retrieves the model name and keyword arguments from the AGENT configuration,
|
|
117
|
+
constructs a SambaStudio LLM instance, and returns it.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
LLM
|
|
122
|
+
An initialized LLM object configured using the `AGENT` settings.
|
|
123
|
+
"""
|
|
124
|
+
_model_kwargs = AGENT.get("model_kwargs", {}).copy()
|
|
125
|
+
if model_kwargs is not None:
|
|
126
|
+
for k in model_kwargs:
|
|
127
|
+
_model_kwargs[k] = model_kwargs[k]
|
|
128
|
+
|
|
129
|
+
if "model" not in _model_kwargs:
|
|
130
|
+
_model_kwargs["model"] = AGENT.get("model", model_name)
|
|
131
|
+
|
|
132
|
+
if service_provider:
|
|
133
|
+
_service_provider = service_provider
|
|
134
|
+
else:
|
|
135
|
+
_service_provider = AGENT.get("service_provider")
|
|
136
|
+
|
|
137
|
+
if _service_provider == "sambanova":
|
|
138
|
+
from langchain_community.llms.sambanova import SambaStudio
|
|
139
|
+
|
|
140
|
+
os.environ["SAMBASTUDIO_URL"] = AGENT.get("llm_server_url")
|
|
141
|
+
os.environ["SAMBASTUDIO_API_KEY"] = AGENT.get("api_key")
|
|
142
|
+
|
|
143
|
+
llm = SambaStudio(model_kwargs=_model_kwargs)
|
|
144
|
+
elif _service_provider == "azure":
|
|
145
|
+
from langchain_openai.chat_models.azure import AzureChatOpenAI
|
|
146
|
+
|
|
147
|
+
api_key = os.environ.get("AZURE_OPENAI_API_KEY", AGENT.get("api_key", None))
|
|
148
|
+
service_url = os.environ.get("AZURE_OPENAI_API_ENDPOINT", AGENT.get("llm_server_url", None))
|
|
149
|
+
llm = AzureChatOpenAI(
|
|
150
|
+
azure_deployment=_model_kwargs.get("model"), azure_endpoint=service_url, api_key=api_key, **_model_kwargs
|
|
151
|
+
)
|
|
152
|
+
elif _service_provider == "openai":
|
|
153
|
+
from langchain_openai import ChatOpenAI
|
|
154
|
+
|
|
155
|
+
api_key = os.environ.get("OPENAI_API_KEY", AGENT.get("api_key", None))
|
|
156
|
+
llm = ChatOpenAI(openai_api_key=api_key, **model_kwargs)
|
|
157
|
+
elif _service_provider == "google":
|
|
158
|
+
if "claude" in _model_kwargs["model"]:
|
|
159
|
+
api_key = os.environ.get("GOOGLE_API_KEY", AGENT.get("api_key", None))
|
|
160
|
+
_model_kwargs["model_id"] = _model_kwargs.pop("model")
|
|
161
|
+
_model_kwargs["google_token_auth"] = api_key
|
|
162
|
+
from flowcept.agents.llms.claude_gcp import ClaudeOnGCPLLM
|
|
163
|
+
|
|
164
|
+
llm = ClaudeOnGCPLLM(**_model_kwargs)
|
|
165
|
+
elif "gemini" in _model_kwargs["model"]:
|
|
166
|
+
from flowcept.agents.llms.gemini25 import Gemini25LLM
|
|
167
|
+
|
|
168
|
+
llm = Gemini25LLM(**_model_kwargs)
|
|
169
|
+
|
|
170
|
+
else:
|
|
171
|
+
raise Exception("Currently supported providers are sambanova, openai, azure, and google.")
|
|
172
|
+
if track_tools:
|
|
173
|
+
llm = FlowceptLLM(llm)
|
|
174
|
+
if agent_id is None:
|
|
175
|
+
agent_id = BaseAgentContextManager.agent_id
|
|
176
|
+
llm.agent_id = agent_id
|
|
177
|
+
if track_tools:
|
|
178
|
+
tool_task = get_current_context_task()
|
|
179
|
+
if tool_task:
|
|
180
|
+
llm.parent_task_id = tool_task.task_id
|
|
181
|
+
return llm
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DynamicSchemaTracker:
|
|
5
|
+
"""
|
|
6
|
+
DynamicSchemaTracker maintains and updates a dynamic schema of tasks,
|
|
7
|
+
tracking both input and output fields along with example values and type
|
|
8
|
+
information. It is designed to help build lightweight, evolving schemas
|
|
9
|
+
from observed task executions.
|
|
10
|
+
|
|
11
|
+
The tracker flattens nested structures, deduplicates fields, and captures
|
|
12
|
+
examples of values (truncated when necessary) to provide insight into
|
|
13
|
+
the shape and types of task data over time.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
max_examples : int, default=3
|
|
18
|
+
Maximum number of example values to store for each field.
|
|
19
|
+
max_str_len : int, default=70
|
|
20
|
+
Maximum string length for stored example values. Longer values are
|
|
21
|
+
truncated with ellipsis.
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
schema : dict
|
|
26
|
+
Maps activity IDs to dictionaries containing lists of input ("i")
|
|
27
|
+
and output ("o") fields. Example:
|
|
28
|
+
``{"train_model": {"i": ["used.dataset"], "o": ["generated.metrics"]}}``.
|
|
29
|
+
values : dict
|
|
30
|
+
Maps normalized field names to metadata about their values, including:
|
|
31
|
+
- ``v`` : list of example values (up to ``max_examples``).
|
|
32
|
+
- ``t`` : type of the field ("int", "float", "list", "str", etc.).
|
|
33
|
+
- ``s`` : shape of lists (if applicable).
|
|
34
|
+
- ``et`` : element type for lists (if applicable).
|
|
35
|
+
max_examples : int
|
|
36
|
+
Maximum number of examples per field.
|
|
37
|
+
max_str_len : int
|
|
38
|
+
Maximum stored string length for example values.
|
|
39
|
+
|
|
40
|
+
Methods
|
|
41
|
+
-------
|
|
42
|
+
update_with_tasks(tasks)
|
|
43
|
+
Update the schema and value examples with a list of tasks.
|
|
44
|
+
get_schema()
|
|
45
|
+
Retrieve the current schema with prefixed "used." and "generated." fields.
|
|
46
|
+
get_example_values()
|
|
47
|
+
Retrieve deduplicated example values and type information for fields.
|
|
48
|
+
|
|
49
|
+
Examples
|
|
50
|
+
--------
|
|
51
|
+
>>> tracker = DynamicSchemaTracker(max_examples=2, max_str_len=20)
|
|
52
|
+
>>> tasks = [
|
|
53
|
+
... {"activity_id": "task1",
|
|
54
|
+
... "used": {"input": [1, 2, 3]},
|
|
55
|
+
... "generated": {"output": {"score": 0.95}}}
|
|
56
|
+
... ]
|
|
57
|
+
>>> tracker.update_with_tasks(tasks)
|
|
58
|
+
>>> tracker.get_schema()
|
|
59
|
+
{'task1': {'i': ['used.input'], 'o': ['generated.output.score']}}
|
|
60
|
+
>>> tracker.get_example_values()
|
|
61
|
+
{'input': {'v': [[1, 2, 3]], 't': 'list', 's': [3], 'et': 'int'},
|
|
62
|
+
'output.score': {'v': [0.95], 't': 'float'}}
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, max_examples=3, max_str_len=70):
|
|
66
|
+
self.schema = {} # {activity_id: {"i": [...], "o": [...]}}
|
|
67
|
+
|
|
68
|
+
# {normalized_field: {"v": [...], "t": ..., "s": ..., "et": ...}}
|
|
69
|
+
self.values = {}
|
|
70
|
+
|
|
71
|
+
self.max_examples = max_examples
|
|
72
|
+
self.max_str_len = max_str_len
|
|
73
|
+
|
|
74
|
+
def _flatten_dict(self, d, parent_key="", sep="."):
|
|
75
|
+
"""Flatten dictionary but preserve lists as single units."""
|
|
76
|
+
items = []
|
|
77
|
+
for k, v in d.items():
|
|
78
|
+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
|
79
|
+
if isinstance(v, dict):
|
|
80
|
+
items.extend(self._flatten_dict(v, new_key, sep=sep).items())
|
|
81
|
+
else:
|
|
82
|
+
items.append((new_key, v))
|
|
83
|
+
return dict(items)
|
|
84
|
+
|
|
85
|
+
def _truncate_if_needed(self, val):
|
|
86
|
+
"""Truncate if stringified length exceeds max_str_len."""
|
|
87
|
+
try:
|
|
88
|
+
s = json.dumps(val)
|
|
89
|
+
except Exception:
|
|
90
|
+
s = str(val)
|
|
91
|
+
|
|
92
|
+
if len(s) > self.max_str_len:
|
|
93
|
+
return s[: self.max_str_len] + "..."
|
|
94
|
+
return val
|
|
95
|
+
|
|
96
|
+
def _get_type(self, val):
|
|
97
|
+
if isinstance(val, bool):
|
|
98
|
+
return "bool"
|
|
99
|
+
elif isinstance(val, int):
|
|
100
|
+
return "int"
|
|
101
|
+
elif isinstance(val, float):
|
|
102
|
+
return "float"
|
|
103
|
+
elif isinstance(val, list):
|
|
104
|
+
return "list"
|
|
105
|
+
else:
|
|
106
|
+
return "str"
|
|
107
|
+
|
|
108
|
+
def _get_shape(self, val):
|
|
109
|
+
if not isinstance(val, list):
|
|
110
|
+
return None
|
|
111
|
+
shape = []
|
|
112
|
+
while isinstance(val, list):
|
|
113
|
+
shape.append(len(val))
|
|
114
|
+
if not val: # Empty list -> stop
|
|
115
|
+
break
|
|
116
|
+
val = val[0]
|
|
117
|
+
return shape
|
|
118
|
+
|
|
119
|
+
def _get_list_element_type(self, val):
|
|
120
|
+
if not isinstance(val, list):
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def describe(elem):
|
|
124
|
+
if isinstance(elem, list):
|
|
125
|
+
return f"list[{describe(elem[0])}]" if elem else "list[unknown]"
|
|
126
|
+
elif isinstance(elem, dict):
|
|
127
|
+
return "dict"
|
|
128
|
+
elif isinstance(elem, bool):
|
|
129
|
+
return "bool"
|
|
130
|
+
elif isinstance(elem, int):
|
|
131
|
+
return "int"
|
|
132
|
+
elif isinstance(elem, float):
|
|
133
|
+
return "float"
|
|
134
|
+
elif isinstance(elem, str):
|
|
135
|
+
return "str"
|
|
136
|
+
else:
|
|
137
|
+
return "unknown"
|
|
138
|
+
|
|
139
|
+
return describe(val[0]) if val else "unknown"
|
|
140
|
+
|
|
141
|
+
def _add_schema_field(self, activity_id, field_name, direction):
|
|
142
|
+
key = "i" if direction == "used" else "o"
|
|
143
|
+
if field_name not in self.schema[activity_id][key]:
|
|
144
|
+
self.schema[activity_id][key].append(field_name)
|
|
145
|
+
|
|
146
|
+
def _add_value_info(self, normalized_field, val):
|
|
147
|
+
val_type = self._get_type(val)
|
|
148
|
+
truncated_val = self._truncate_if_needed(val)
|
|
149
|
+
|
|
150
|
+
entry = self.values.setdefault(normalized_field, {"v": [], "t": val_type})
|
|
151
|
+
|
|
152
|
+
# Always reflect latest observed type
|
|
153
|
+
entry["t"] = val_type
|
|
154
|
+
|
|
155
|
+
if val_type == "list":
|
|
156
|
+
entry["s"] = self._get_shape(val)
|
|
157
|
+
entry["et"] = self._get_list_element_type(val)
|
|
158
|
+
else:
|
|
159
|
+
entry.pop("s", None)
|
|
160
|
+
entry.pop("et", None)
|
|
161
|
+
|
|
162
|
+
if truncated_val not in entry["v"]:
|
|
163
|
+
entry["v"].append(truncated_val)
|
|
164
|
+
|
|
165
|
+
if len(entry["v"]) > self.max_examples:
|
|
166
|
+
entry["v"] = sorted(entry["v"], key=lambda x: str(x))[: self.max_examples]
|
|
167
|
+
|
|
168
|
+
def update_with_tasks(self, tasks):
|
|
169
|
+
"""Update the schema with tasks."""
|
|
170
|
+
for task in tasks:
|
|
171
|
+
activity = task.get("activity_id")
|
|
172
|
+
if activity not in self.schema:
|
|
173
|
+
self.schema[activity] = {"i": [], "o": []}
|
|
174
|
+
|
|
175
|
+
for direction in ["used", "generated"]:
|
|
176
|
+
data = task.get(direction, {})
|
|
177
|
+
flat_data = self._flatten_dict(data)
|
|
178
|
+
for field, val in flat_data.items():
|
|
179
|
+
prefixed_field = f"{direction}.{field}"
|
|
180
|
+
normalized_field = field # role-agnostic key for value descriptions
|
|
181
|
+
|
|
182
|
+
self._add_schema_field(activity, prefixed_field, direction)
|
|
183
|
+
self._add_value_info(normalized_field, val)
|
|
184
|
+
|
|
185
|
+
def get_schema(self):
|
|
186
|
+
"""Get the current schema."""
|
|
187
|
+
return self.schema # fields with 'used.' or 'generated.' prefix
|
|
188
|
+
|
|
189
|
+
def get_example_values(self):
|
|
190
|
+
"""Get example values."""
|
|
191
|
+
return self.values # deduplicated field schemas
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from threading import Thread
|
|
2
|
+
from time import sleep
|
|
3
|
+
|
|
4
|
+
from flowcept.agents import check_liveness
|
|
5
|
+
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
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""
|
|
15
|
+
Start the MCP server.
|
|
16
|
+
"""
|
|
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
|
+
|
|
20
|
+
def run():
|
|
21
|
+
uvicorn.run(mcp_flowcept.streamable_http_app, host=AGENT_HOST, port=AGENT_PORT, lifespan="on")
|
|
22
|
+
|
|
23
|
+
Thread(target=run).start()
|
|
24
|
+
sleep(2)
|
|
25
|
+
# Wake up tool call
|
|
26
|
+
print(run_tool(check_liveness, host=AGENT_HOST, port=AGENT_PORT)[0])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from flowcept.agents.dynamic_schema_tracker import DynamicSchemaTracker
|
|
2
|
+
from flowcept.agents.tools.in_memory_queries.pandas_agent_utils import load_saved_df
|
|
3
|
+
from flowcept.commons.flowcept_dataclasses.task_object import TaskObject
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os.path
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Dict, List
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from flowcept.flowceptor.consumers.agent.base_agent_context_manager import BaseAgentContextManager, BaseAppContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from flowcept.agents import agent_client
|
|
17
|
+
from flowcept.commons.task_data_preprocess import summarize_task
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FlowceptAppContext(BaseAppContext):
|
|
22
|
+
"""
|
|
23
|
+
Context object for holding flowcept-specific state (e.g., tasks data) during the agent's lifecycle.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
task_summaries : List[Dict]
|
|
28
|
+
List of summarized task dictionaries.
|
|
29
|
+
critical_tasks : List[Dict]
|
|
30
|
+
List of critical task summaries with tags or anomalies.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
tasks: List[Dict] | None
|
|
34
|
+
task_summaries: List[Dict] | None
|
|
35
|
+
critical_tasks: List[Dict] | None
|
|
36
|
+
df: pd.DataFrame | None
|
|
37
|
+
tasks_schema: Dict | None # TODO: we dont need to keep the tasks_schema in context, just in the manager's memory.
|
|
38
|
+
value_examples: Dict | None
|
|
39
|
+
tracker_config: Dict | None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FlowceptAgentContextManager(BaseAgentContextManager):
|
|
43
|
+
"""
|
|
44
|
+
Manages agent context and operations for Flowcept's intelligent task monitoring.
|
|
45
|
+
|
|
46
|
+
This class extends BaseAgentContextManager and maintains a rolling buffer of task messages.
|
|
47
|
+
It summarizes and tags tasks, builds a QA index over them, and uses LLM tools to analyze
|
|
48
|
+
task batches periodically.
|
|
49
|
+
|
|
50
|
+
Attributes
|
|
51
|
+
----------
|
|
52
|
+
context : FlowceptAppContext
|
|
53
|
+
Current application context holding task state and QA components.
|
|
54
|
+
msgs_counter : int
|
|
55
|
+
Counter tracking how many task messages have been processed.
|
|
56
|
+
context_size : int
|
|
57
|
+
Number of task messages to collect before triggering QA index building and LLM analysis.
|
|
58
|
+
qa_manager : FlowceptQAManager
|
|
59
|
+
Utility for constructing QA chains from task summaries.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self.context: FlowceptAppContext = None
|
|
64
|
+
self.tracker_config = dict(max_examples=3, max_str_len=50)
|
|
65
|
+
self.schema_tracker = DynamicSchemaTracker(**self.tracker_config)
|
|
66
|
+
self.msgs_counter = 0
|
|
67
|
+
self.context_size = 1
|
|
68
|
+
super().__init__()
|
|
69
|
+
|
|
70
|
+
def message_handler(self, msg_obj: Dict):
|
|
71
|
+
"""
|
|
72
|
+
Handle an incoming message and update context accordingly.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
msg_obj : Dict
|
|
77
|
+
The incoming message object.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
bool
|
|
82
|
+
True if the message was handled successfully.
|
|
83
|
+
"""
|
|
84
|
+
print("Received:", msg_obj)
|
|
85
|
+
msg_type = msg_obj.get("type", None)
|
|
86
|
+
if msg_type == "task":
|
|
87
|
+
task_msg = TaskObject.from_dict(msg_obj)
|
|
88
|
+
if task_msg.subtype == "llm_task" and task_msg.agent_id == self.agent_id:
|
|
89
|
+
self.logger.info(f"Going to ignore our own LLM messages: {task_msg}")
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
self.msgs_counter += 1
|
|
93
|
+
self.logger.debug("Received task msg!")
|
|
94
|
+
self.context.tasks.append(msg_obj)
|
|
95
|
+
|
|
96
|
+
task_summary = summarize_task(msg_obj, logger=self.logger)
|
|
97
|
+
self.context.task_summaries.append(task_summary)
|
|
98
|
+
if len(task_summary.get("tags", [])):
|
|
99
|
+
self.context.critical_tasks.append(task_summary)
|
|
100
|
+
|
|
101
|
+
if self.msgs_counter > 0 and self.msgs_counter % self.context_size == 0:
|
|
102
|
+
self.logger.debug(
|
|
103
|
+
f"Going to add to index! {(self.msgs_counter - self.context_size, self.msgs_counter)}"
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
self.update_schema_and_add_to_df(
|
|
107
|
+
tasks=self.context.task_summaries[self.msgs_counter - self.context_size : self.msgs_counter]
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.error(
|
|
111
|
+
f"Could not add these tasks to buffer!\n"
|
|
112
|
+
f"{self.context.task_summaries[self.msgs_counter - self.context_size : self.msgs_counter]}"
|
|
113
|
+
)
|
|
114
|
+
self.logger.exception(e)
|
|
115
|
+
|
|
116
|
+
# self.monitor_chunk()
|
|
117
|
+
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
def update_schema_and_add_to_df(self, tasks: List[Dict]):
|
|
121
|
+
"""Update the schema and add to the DataFrame in context."""
|
|
122
|
+
self.schema_tracker.update_with_tasks(tasks)
|
|
123
|
+
self.context.tasks_schema = self.schema_tracker.get_schema()
|
|
124
|
+
self.context.value_examples = self.schema_tracker.get_example_values()
|
|
125
|
+
|
|
126
|
+
_df = pd.json_normalize(tasks)
|
|
127
|
+
self.context.df = pd.concat([self.context.df, pd.DataFrame(_df)], ignore_index=True)
|
|
128
|
+
|
|
129
|
+
def monitor_chunk(self):
|
|
130
|
+
"""
|
|
131
|
+
Perform LLM-based analysis on the current chunk of task messages and send the results.
|
|
132
|
+
"""
|
|
133
|
+
self.logger.debug(f"Going to begin LLM job! {self.msgs_counter}")
|
|
134
|
+
result = agent_client.run_tool("analyze_task_chunk")
|
|
135
|
+
if len(result):
|
|
136
|
+
content = result[0].text
|
|
137
|
+
if content != "Error executing tool":
|
|
138
|
+
msg = {"type": "flowcept_agent", "info": "monitor", "content": content}
|
|
139
|
+
self._mq_dao.send_message(msg)
|
|
140
|
+
self.logger.debug(str(content))
|
|
141
|
+
else:
|
|
142
|
+
self.logger.error(content)
|
|
143
|
+
|
|
144
|
+
def reset_context(self):
|
|
145
|
+
"""
|
|
146
|
+
Reset the agent's context to a clean state, initializing a new QA setup.
|
|
147
|
+
"""
|
|
148
|
+
self.context = FlowceptAppContext(
|
|
149
|
+
tasks=[],
|
|
150
|
+
task_summaries=[],
|
|
151
|
+
critical_tasks=[],
|
|
152
|
+
df=pd.DataFrame(),
|
|
153
|
+
tasks_schema={},
|
|
154
|
+
value_examples={},
|
|
155
|
+
tracker_config=self.tracker_config,
|
|
156
|
+
)
|
|
157
|
+
DEBUG = True # TODO debugging!
|
|
158
|
+
if DEBUG:
|
|
159
|
+
self.logger.warning("Running agent in DEBUG mode!")
|
|
160
|
+
df_path = "/tmp/current_agent_df.csv"
|
|
161
|
+
if os.path.exists(df_path):
|
|
162
|
+
self.logger.warning("Going to load df into context")
|
|
163
|
+
df = load_saved_df(df_path)
|
|
164
|
+
self.context.df = df
|
|
165
|
+
if os.path.exists("/tmp/current_tasks_schema.json"):
|
|
166
|
+
with open("/tmp/current_tasks_schema.json") as f:
|
|
167
|
+
self.context.tasks_schema = json.load(f)
|
|
168
|
+
if os.path.exists("/tmp/value_examples.json"):
|
|
169
|
+
with open("/tmp/value_examples.json") as f:
|
|
170
|
+
self.context.value_examples = json.load(f)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Exporting the ctx_manager and the mcp_flowcept
|
|
174
|
+
ctx_manager = FlowceptAgentContextManager()
|
|
175
|
+
mcp_flowcept = FastMCP("FlowceptAgent", require_session=False, lifespan=ctx_manager.lifespan, stateless_http=True)
|