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.
Files changed (56) hide show
  1. flowcept/__init__.py +7 -4
  2. flowcept/agents/__init__.py +5 -0
  3. flowcept/{flowceptor/consumers/agent/client_agent.py → agents/agent_client.py} +22 -12
  4. flowcept/agents/agents_utils.py +181 -0
  5. flowcept/agents/dynamic_schema_tracker.py +191 -0
  6. flowcept/agents/flowcept_agent.py +30 -0
  7. flowcept/agents/flowcept_ctx_manager.py +175 -0
  8. flowcept/agents/gui/__init__.py +5 -0
  9. flowcept/agents/gui/agent_gui.py +76 -0
  10. flowcept/agents/gui/gui_utils.py +239 -0
  11. flowcept/agents/llms/__init__.py +1 -0
  12. flowcept/agents/llms/claude_gcp.py +139 -0
  13. flowcept/agents/llms/gemini25.py +119 -0
  14. flowcept/agents/prompts/__init__.py +1 -0
  15. flowcept/{flowceptor/adapters/agents/prompts.py → agents/prompts/general_prompts.py} +18 -0
  16. flowcept/agents/prompts/in_memory_query_prompts.py +297 -0
  17. flowcept/agents/tools/__init__.py +1 -0
  18. flowcept/agents/tools/general_tools.py +102 -0
  19. flowcept/agents/tools/in_memory_queries/__init__.py +1 -0
  20. flowcept/agents/tools/in_memory_queries/in_memory_queries_tools.py +704 -0
  21. flowcept/agents/tools/in_memory_queries/pandas_agent_utils.py +309 -0
  22. flowcept/cli.py +286 -44
  23. flowcept/commons/daos/docdb_dao/mongodb_dao.py +47 -0
  24. flowcept/commons/daos/mq_dao/mq_dao_base.py +24 -13
  25. flowcept/commons/daos/mq_dao/mq_dao_kafka.py +18 -2
  26. flowcept/commons/flowcept_dataclasses/task_object.py +16 -21
  27. flowcept/commons/flowcept_dataclasses/workflow_object.py +9 -1
  28. flowcept/commons/task_data_preprocess.py +260 -60
  29. flowcept/commons/utils.py +25 -6
  30. flowcept/configs.py +41 -26
  31. flowcept/flowcept_api/flowcept_controller.py +73 -6
  32. flowcept/flowceptor/adapters/base_interceptor.py +11 -5
  33. flowcept/flowceptor/consumers/agent/base_agent_context_manager.py +25 -1
  34. flowcept/flowceptor/consumers/base_consumer.py +4 -0
  35. flowcept/flowceptor/consumers/consumer_utils.py +5 -4
  36. flowcept/flowceptor/consumers/document_inserter.py +2 -2
  37. flowcept/flowceptor/telemetry_capture.py +5 -2
  38. flowcept/instrumentation/flowcept_agent_task.py +294 -0
  39. flowcept/instrumentation/flowcept_decorator.py +43 -0
  40. flowcept/instrumentation/flowcept_loop.py +3 -3
  41. flowcept/instrumentation/flowcept_task.py +64 -24
  42. flowcept/instrumentation/flowcept_torch.py +5 -5
  43. flowcept/instrumentation/task_capture.py +83 -6
  44. flowcept/version.py +1 -1
  45. {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/METADATA +42 -14
  46. {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/RECORD +50 -36
  47. resources/sample_settings.yaml +12 -4
  48. flowcept/flowceptor/adapters/agents/__init__.py +0 -1
  49. flowcept/flowceptor/adapters/agents/agents_utils.py +0 -89
  50. flowcept/flowceptor/adapters/agents/flowcept_agent.py +0 -292
  51. flowcept/flowceptor/adapters/agents/flowcept_llm_prov_capture.py +0 -186
  52. flowcept/flowceptor/consumers/agent/flowcept_agent_context_manager.py +0 -145
  53. flowcept/flowceptor/consumers/agent/flowcept_qa_manager.py +0 -112
  54. {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/WHEEL +0 -0
  55. {flowcept-0.8.11.dist-info → flowcept-0.9.1.dist-info}/entry_points.txt +0 -0
  56. {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
 
@@ -0,0 +1,5 @@
1
+ # flake8: noqa: F403
2
+ """Agents subpackage."""
3
+
4
+ from flowcept.agents.tools.general_tools import *
5
+ from flowcept.agents.tools.in_memory_queries.in_memory_queries_tools import *
@@ -1,17 +1,15 @@
1
- from typing import Dict, List
1
+ import asyncio
2
+ from typing import Dict, List, Callable
2
3
 
3
- from flowcept.configs import AGENT
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
- MCP_URL = f"http://{MCP_HOST}:{MCP_PORT}/mcp"
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
- import asyncio
37
+ if isinstance(tool_name, Callable):
38
+ tool_name = tool_name.__name__
40
39
 
41
40
  async def _run():
42
- async with streamablehttp_client(MCP_URL) as (read, write, _):
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
- return result.content
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)
@@ -0,0 +1,5 @@
1
+ """Streamlit Subpackage."""
2
+
3
+ PAGE_TITLE = "Flowcept Agent Chat"
4
+ DEFAULT_AGENT_NAME = "FlowceptAgent"
5
+ AI = "🤖"