fewsvisualiser 1.0.1__tar.gz
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.
- fewsvisualiser-1.0.1/.github/workflows/python-publish.yml +40 -0
- fewsvisualiser-1.0.1/.gitignore +19 -0
- fewsvisualiser-1.0.1/.vscode/settings.json +10 -0
- fewsvisualiser-1.0.1/ConfigVisDatamodel.png +0 -0
- fewsvisualiser-1.0.1/PKG-INFO +54 -0
- fewsvisualiser-1.0.1/README.md +41 -0
- fewsvisualiser-1.0.1/app/components/__init__.py +10 -0
- fewsvisualiser-1.0.1/app/components/sidebar_chat.py +327 -0
- fewsvisualiser-1.0.1/app/components/tab_explore.py +91 -0
- fewsvisualiser-1.0.1/app/components/tab_reports.py +284 -0
- fewsvisualiser-1.0.1/app/components/tools_interface.py +137 -0
- fewsvisualiser-1.0.1/app/streamlit_app.py +56 -0
- fewsvisualiser-1.0.1/app/utils.py +26 -0
- fewsvisualiser-1.0.1/app_scope.md +141 -0
- fewsvisualiser-1.0.1/configvis.ipynb +200 -0
- fewsvisualiser-1.0.1/db/.gitkeep +1 -0
- fewsvisualiser-1.0.1/details.md +544 -0
- fewsvisualiser-1.0.1/doc/FEWS visualizer.pptx +0 -0
- fewsvisualiser-1.0.1/doc/aantekeningen.docx +0 -0
- fewsvisualiser-1.0.1/doc/queries.txt +41 -0
- fewsvisualiser-1.0.1/docker/Dockerfile +32 -0
- fewsvisualiser-1.0.1/env/.env.example +26 -0
- fewsvisualiser-1.0.1/fews-networkvisualisation.code-workspace +8 -0
- fewsvisualiser-1.0.1/fewsvisualiser/__init__.py +11 -0
- fewsvisualiser-1.0.1/fewsvisualiser/__main__.py +8 -0
- fewsvisualiser-1.0.1/fewsvisualiser/analyse.py +61 -0
- fewsvisualiser-1.0.1/fewsvisualiser/core/__init__.py +0 -0
- fewsvisualiser-1.0.1/fewsvisualiser/core/dbconfigvis.py +401 -0
- fewsvisualiser-1.0.1/fewsvisualiser/core/dblocset.py +79 -0
- fewsvisualiser-1.0.1/fewsvisualiser/core/fewsparsers.py +543 -0
- fewsvisualiser-1.0.1/fewsvisualiser/core/helpers.py +100 -0
- fewsvisualiser-1.0.1/fewsvisualiser/core/locsetparser.py +76 -0
- fewsvisualiser-1.0.1/fewsvisualiser/explore.py +123 -0
- fewsvisualiser-1.0.1/fewsvisualiser/locset_analysis.py +43 -0
- fewsvisualiser-1.0.1/notebooks/analyze_endpoints.py +240 -0
- fewsvisualiser-1.0.1/notebooks/talkwithfews.ipynb +398 -0
- fewsvisualiser-1.0.1/notebooks/validate_tools.ipynb +304 -0
- fewsvisualiser-1.0.1/pixi.lock +10640 -0
- fewsvisualiser-1.0.1/pixi.toml +33 -0
- fewsvisualiser-1.0.1/pyproject.toml +21 -0
- fewsvisualiser-1.0.1/scope.md +75 -0
- fewsvisualiser-1.0.1/scripts/README.md +6 -0
- fewsvisualiser-1.0.1/scripts/run_visualiser.ipynb +140 -0
- fewsvisualiser-1.0.1/scripts/run_visualiser.py +23 -0
- fewsvisualiser-1.0.1/talkwithfews/README.md +59 -0
- fewsvisualiser-1.0.1/talkwithfews/__init__.py +8 -0
- fewsvisualiser-1.0.1/talkwithfews/agent.py +244 -0
- fewsvisualiser-1.0.1/talkwithfews/logging_utils.py +143 -0
- fewsvisualiser-1.0.1/talkwithfews/models.py +492 -0
- fewsvisualiser-1.0.1/talkwithfews/schema_utils.py +337 -0
- fewsvisualiser-1.0.1/talkwithfews/tools.py +1425 -0
- fewsvisualiser-1.0.1/talkwithfews/utils.py +138 -0
- fewsvisualiser-1.0.1/tests/__init__.py +0 -0
- fewsvisualiser-1.0.1/tests/files/.gitignore +1 -0
- fewsvisualiser-1.0.1/tests/files/.gitkeep +1 -0
- fewsvisualiser-1.0.1/tests/test_core.py +26 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# This workflow will upload a Python Package using Twine when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
deploy:
|
|
21
|
+
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
# Specifying a GitHub environment is optional, but strongly encouraged
|
|
24
|
+
environment: release
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v6
|
|
27
|
+
- name: Set up Python
|
|
28
|
+
uses: actions/setup-python@v6
|
|
29
|
+
with:
|
|
30
|
+
python-version: '3.x'
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: |
|
|
33
|
+
python -m pip install --upgrade pip
|
|
34
|
+
pip install build
|
|
35
|
+
- name: Build package
|
|
36
|
+
run: python -m build
|
|
37
|
+
- name: Publish package
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
39
|
+
with:
|
|
40
|
+
packages-dir: dist
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/db/*.db3
|
|
2
|
+
__pycache__
|
|
3
|
+
aantekeningen.doc
|
|
4
|
+
test*.ipynb
|
|
5
|
+
/.vscode/launch.json
|
|
6
|
+
/config
|
|
7
|
+
.pixi
|
|
8
|
+
.DS_Store
|
|
9
|
+
db/*.log
|
|
10
|
+
notebooks/*
|
|
11
|
+
!notebooks/*.py
|
|
12
|
+
!notebooks/talkwithfews.ipynb
|
|
13
|
+
!notebooks/validate_tools.ipynb
|
|
14
|
+
env/.env
|
|
15
|
+
logs/fews_agent.log
|
|
16
|
+
doc/TalkWithFEWS-7april-GO-FEWS.key
|
|
17
|
+
hidden_*
|
|
18
|
+
doc/TalkWithFEWS-7april-GO-FEWS.pdf
|
|
19
|
+
dist
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"python.defaultInterpreterPath": ".pixi/envs/default3/python.exe",
|
|
3
|
+
"python.testing.pytestArgs": [
|
|
4
|
+
"tests"
|
|
5
|
+
],
|
|
6
|
+
"python.testing.unittestEnabled": false,
|
|
7
|
+
"python.testing.pytestEnabled": true,
|
|
8
|
+
"search.useGlobalIgnoreFiles": true,
|
|
9
|
+
"python-envs.defaultEnvManager": "ms-python.python:system"
|
|
10
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fewsvisualiser
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: FEWSVisualiser: Insights for optimized Delft-FEWS configurations.
|
|
5
|
+
Author-email: Job Verkaik <verkaik@hkv.nl>
|
|
6
|
+
Keywords: configuration,declarative,delft-fews,insight
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Requires-Dist: fire
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# FEWSVisualiser: Insights for optimized Delft-FEWS configurations
|
|
15
|
+
|
|
16
|
+
A differerent approach to the analysis and visualisation of Delft-FEWS configuration
|
|
17
|
+
|
|
18
|
+
This project is a successor of [FEWS_Netwerkvisualisation](https://github.com/HKV-products-services/FEWS_Netwerkvisualisation)
|
|
19
|
+
|
|
20
|
+
HKV project
|
|
21
|
+
RO1122.10.00 Herstructurering analyse netwerkvisualisatie
|
|
22
|
+
|
|
23
|
+
## Talk with fews
|
|
24
|
+
|
|
25
|
+
> [!CAUTION]
|
|
26
|
+
> De eerste ontiwikkelingen van `talk with FEWS` waren in deze repo onder `/talkwithfews`.
|
|
27
|
+
> Dit is echter losgetrokken, de actuele repo is:  te vinden.
|
|
28
|
+
> Actiepunt: later volledig lostrekken.
|
|
29
|
+
|
|
30
|
+
## aantekeningen
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
2024-02-11
|
|
35
|
+
|
|
36
|
+
Zie explore. Waarom zijn er 5 call stacks met wf_DebietBerekening?
|
|
37
|
+
|
|
38
|
+
2024-02-12
|
|
39
|
+
|
|
40
|
+
Die call stacks zijn wsl goed. Elke module binnen een wf krijgt namelijk een eigen call stack en dat is terecht. Alleen dit deel moet even goed anders: de volgorde van het aanroepen van modules binnen en wf is van belang wanneer er temporaries worden doorgegeven naar een volgende module. Dit is een case die nu nog niet ondersteund wordt.
|
|
41
|
+
|
|
42
|
+
De betere manier is om in de call stacks per wf vanuit de descriptor de volgorde van modins call vast te leggen. Temporaries kunnen dan op een andere manier door gegeven worden en hoeven niet de database in.
|
|
43
|
+
|
|
44
|
+
## How to run?
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pixi install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pixi run analyse_config {path_to_config} {path_to_db}
|
|
52
|
+
# e.g:
|
|
53
|
+
pixi run analyse_config "..\FEWS Digiflow\Config" "db\digiflow.db3"
|
|
54
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# FEWSVisualiser: Insights for optimized Delft-FEWS configurations
|
|
2
|
+
|
|
3
|
+
A differerent approach to the analysis and visualisation of Delft-FEWS configuration
|
|
4
|
+
|
|
5
|
+
This project is a successor of [FEWS_Netwerkvisualisation](https://github.com/HKV-products-services/FEWS_Netwerkvisualisation)
|
|
6
|
+
|
|
7
|
+
HKV project
|
|
8
|
+
RO1122.10.00 Herstructurering analyse netwerkvisualisatie
|
|
9
|
+
|
|
10
|
+
## Talk with fews
|
|
11
|
+
|
|
12
|
+
> [!CAUTION]
|
|
13
|
+
> De eerste ontiwikkelingen van `talk with FEWS` waren in deze repo onder `/talkwithfews`.
|
|
14
|
+
> Dit is echter losgetrokken, de actuele repo is:  te vinden.
|
|
15
|
+
> Actiepunt: later volledig lostrekken.
|
|
16
|
+
|
|
17
|
+
## aantekeningen
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
2024-02-11
|
|
22
|
+
|
|
23
|
+
Zie explore. Waarom zijn er 5 call stacks met wf_DebietBerekening?
|
|
24
|
+
|
|
25
|
+
2024-02-12
|
|
26
|
+
|
|
27
|
+
Die call stacks zijn wsl goed. Elke module binnen een wf krijgt namelijk een eigen call stack en dat is terecht. Alleen dit deel moet even goed anders: de volgorde van het aanroepen van modules binnen en wf is van belang wanneer er temporaries worden doorgegeven naar een volgende module. Dit is een case die nu nog niet ondersteund wordt.
|
|
28
|
+
|
|
29
|
+
De betere manier is om in de call stacks per wf vanuit de descriptor de volgorde van modins call vast te leggen. Temporaries kunnen dan op een andere manier door gegeven worden en hoeven niet de database in.
|
|
30
|
+
|
|
31
|
+
## How to run?
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pixi install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pixi run analyse_config {path_to_config} {path_to_db}
|
|
39
|
+
# e.g:
|
|
40
|
+
pixi run analyse_config "..\FEWS Digiflow\Config" "db\digiflow.db3"
|
|
41
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .sidebar_chat import init_chat_state, render_chat_sidebar
|
|
2
|
+
from .tab_explore import render_explore_tab
|
|
3
|
+
from .tab_reports import render_reports_tab
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'init_chat_state',
|
|
7
|
+
'render_chat_sidebar',
|
|
8
|
+
'render_explore_tab',
|
|
9
|
+
'render_reports_tab'
|
|
10
|
+
]
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
# Add parent directory to path for imports
|
|
4
|
+
sys.path.append(str(Path(__file__).parent.parent.parent))
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
from streamlit_markdown import st_markdown
|
|
8
|
+
from talkwithfews.agent import FEWSConfigAgent
|
|
9
|
+
import asyncio
|
|
10
|
+
from queue import Queue, Empty
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
# Set up logging
|
|
16
|
+
logging.basicConfig(level=logging.INFO)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class AgentManager:
|
|
20
|
+
"""Manages the FEWS agent in a separate thread"""
|
|
21
|
+
def __init__(self, config):
|
|
22
|
+
self.config = config
|
|
23
|
+
self.agent = None
|
|
24
|
+
self.response_queue = Queue()
|
|
25
|
+
self.command_queue = Queue()
|
|
26
|
+
self.thread = None
|
|
27
|
+
self.ready = threading.Event()
|
|
28
|
+
self.processing = False
|
|
29
|
+
self.error = None
|
|
30
|
+
|
|
31
|
+
def initialize(self):
|
|
32
|
+
"""Initialize the agent in a dedicated thread"""
|
|
33
|
+
def agent_thread():
|
|
34
|
+
try:
|
|
35
|
+
# Create and initialize agent
|
|
36
|
+
self.agent = FEWSConfigAgent(
|
|
37
|
+
db_path=self.config["db_path"],
|
|
38
|
+
log_dir=self.config["log_dir"]
|
|
39
|
+
)
|
|
40
|
+
self.agent.connect()
|
|
41
|
+
|
|
42
|
+
# Set up event loop
|
|
43
|
+
loop = asyncio.new_event_loop()
|
|
44
|
+
asyncio.set_event_loop(loop)
|
|
45
|
+
|
|
46
|
+
# Signal ready
|
|
47
|
+
self.ready.set()
|
|
48
|
+
|
|
49
|
+
while True:
|
|
50
|
+
try:
|
|
51
|
+
command = self.command_queue.get(timeout=1)
|
|
52
|
+
if command is None: # Shutdown signal
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
self.processing = True
|
|
56
|
+
self.error = None
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
response = loop.run_until_complete(asyncio.wait_for(
|
|
60
|
+
self.agent.ask(command),
|
|
61
|
+
timeout=300
|
|
62
|
+
))
|
|
63
|
+
self.response_queue.put(response)
|
|
64
|
+
except asyncio.TimeoutError:
|
|
65
|
+
self.error = "Request timed out after 5 minutes"
|
|
66
|
+
self.response_queue.put(self.error)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
self.error = str(e)
|
|
69
|
+
self.response_queue.put(f"Error: {str(e)}")
|
|
70
|
+
finally:
|
|
71
|
+
self.processing = False
|
|
72
|
+
|
|
73
|
+
except Empty:
|
|
74
|
+
continue
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self.error = str(e)
|
|
77
|
+
self.response_queue.put(f"Error: {str(e)}")
|
|
78
|
+
self.processing = False
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
self.error = str(e)
|
|
82
|
+
self.response_queue.put(f"Error: {str(e)}")
|
|
83
|
+
finally:
|
|
84
|
+
try:
|
|
85
|
+
loop.close()
|
|
86
|
+
except:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
self.thread = threading.Thread(target=agent_thread, daemon=True)
|
|
90
|
+
self.thread.start()
|
|
91
|
+
if not self.ready.wait(timeout=10):
|
|
92
|
+
raise Exception("Agent initialization timed out")
|
|
93
|
+
|
|
94
|
+
def ask(self, prompt):
|
|
95
|
+
"""Send a question to the agent thread"""
|
|
96
|
+
if not self.thread or not self.thread.is_alive():
|
|
97
|
+
raise Exception("Agent thread is not running")
|
|
98
|
+
|
|
99
|
+
self.command_queue.put(prompt)
|
|
100
|
+
start_time = time.time()
|
|
101
|
+
|
|
102
|
+
while time.time() - start_time < 300: # 5 minutes timeout
|
|
103
|
+
try:
|
|
104
|
+
# Wait for response with a short timeout
|
|
105
|
+
response = self.response_queue.get(timeout=1)
|
|
106
|
+
if isinstance(response, str) and response.startswith("Error:"):
|
|
107
|
+
raise Exception(response)
|
|
108
|
+
return response
|
|
109
|
+
except Empty:
|
|
110
|
+
# If we're still processing, keep waiting
|
|
111
|
+
if self.processing:
|
|
112
|
+
continue
|
|
113
|
+
# If we have an error, raise it
|
|
114
|
+
if self.error:
|
|
115
|
+
raise Exception(self.error)
|
|
116
|
+
# Otherwise, keep waiting
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
raise Exception("Timeout waiting for response")
|
|
120
|
+
|
|
121
|
+
def close(self):
|
|
122
|
+
"""Close the agent and cleanup"""
|
|
123
|
+
if self.thread and self.thread.is_alive():
|
|
124
|
+
self.command_queue.put(None)
|
|
125
|
+
self.thread.join(timeout=5)
|
|
126
|
+
self.agent = None
|
|
127
|
+
self.thread = None
|
|
128
|
+
self.ready.clear()
|
|
129
|
+
self.processing = False
|
|
130
|
+
self.error = None
|
|
131
|
+
|
|
132
|
+
def init_chat_state():
|
|
133
|
+
"""Initialize chat state and agent if not exists"""
|
|
134
|
+
if "messages" not in st.session_state:
|
|
135
|
+
st.session_state.messages = []
|
|
136
|
+
if "agent_manager" not in st.session_state:
|
|
137
|
+
st.session_state.agent_manager = None
|
|
138
|
+
if "selected_data" not in st.session_state:
|
|
139
|
+
st.session_state.selected_data = None
|
|
140
|
+
|
|
141
|
+
def format_agent_response(response):
|
|
142
|
+
"""Format the agent response for display"""
|
|
143
|
+
if hasattr(response, 'data'):
|
|
144
|
+
content = {
|
|
145
|
+
"text": response.data.markdown_text or "",
|
|
146
|
+
"table": response.data.markdown_table or "",
|
|
147
|
+
"diagram": response.data.mermaid_diagram or ""
|
|
148
|
+
}
|
|
149
|
+
else:
|
|
150
|
+
content = {"text": str(response), "table": "", "diagram": ""}
|
|
151
|
+
return content
|
|
152
|
+
|
|
153
|
+
def add_to_chat(data: dict):
|
|
154
|
+
"""
|
|
155
|
+
Add FEWS object data to the chat as a question and get agent response.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
data: Dictionary containing FEWS object information with at least a 'type' field
|
|
159
|
+
Supported types:
|
|
160
|
+
- workflow: Contains Workflow ID, Beschrijving, Module Config IDs
|
|
161
|
+
- timeseries: Contains ID, Module, Parameter info for timeseries
|
|
162
|
+
Future support planned for:
|
|
163
|
+
- transformation: Contains transformation details
|
|
164
|
+
"""
|
|
165
|
+
if "messages" not in st.session_state:
|
|
166
|
+
st.session_state.messages = []
|
|
167
|
+
|
|
168
|
+
# Format question based on object type
|
|
169
|
+
if data.get("type") == "timeseries":
|
|
170
|
+
# For timeseries sets
|
|
171
|
+
ts_id = data.get("ID")
|
|
172
|
+
module = data.get("Module", "")
|
|
173
|
+
parameter = data.get("Parameter", "")
|
|
174
|
+
question = (
|
|
175
|
+
f"Kun je me meer vertellen over de tijdreeks met ID {ts_id}? "
|
|
176
|
+
f"Deze tijdreeks is van parameter '{parameter}' in module '{module}'. "
|
|
177
|
+
f"Wat zijn de producerende transformaties en waar wordt deze tijdreeks voor gebruikt?"
|
|
178
|
+
f"Met andere woorden, waarom is dit een dood punt in de configuratie?"
|
|
179
|
+
)
|
|
180
|
+
elif data.get("type") == "workflow":
|
|
181
|
+
# For workflows (renamed from the default case)
|
|
182
|
+
workflow_id = data.get("Workflow ID")
|
|
183
|
+
if not workflow_id:
|
|
184
|
+
st.error("Geen workflow ID gevonden in de data")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# Format the workflow data into a clear question
|
|
188
|
+
question = (
|
|
189
|
+
f"Kun je me meer vertellen over de workflow met ID {workflow_id}? "
|
|
190
|
+
f"Deze workflow heeft als beschrijving: '{data.get('Beschrijving', 'Geen beschrijving')}' "
|
|
191
|
+
f"en gebruikt de volgende Module Config IDs: {data.get('Module Config IDs', [])}"
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
st.error(f"Niet-ondersteund object type: {data.get('type', 'onbekend')}")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Add the question to the chat history
|
|
198
|
+
st.session_state.messages.append({
|
|
199
|
+
"role": "user",
|
|
200
|
+
"content": question
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
# Store the question in session state to process after rerun
|
|
204
|
+
if "pending_question" not in st.session_state:
|
|
205
|
+
st.session_state.pending_question = None
|
|
206
|
+
st.session_state.pending_question = question
|
|
207
|
+
|
|
208
|
+
# Trigger a rerun to update the UI with the question
|
|
209
|
+
st.rerun()
|
|
210
|
+
|
|
211
|
+
def show_table_dialog(table_content: str, title: str = "Table Details"):
|
|
212
|
+
"""Display a table in a popover."""
|
|
213
|
+
with st.popover(title):
|
|
214
|
+
st.markdown(table_content)
|
|
215
|
+
if st.button("Close", use_container_width=True):
|
|
216
|
+
st.rerun()
|
|
217
|
+
|
|
218
|
+
def format_mermaid_diagram(diagram_content: str) -> str:
|
|
219
|
+
"""Format the diagram content to ensure proper mermaid syntax."""
|
|
220
|
+
if not diagram_content.startswith("```mermaid"):
|
|
221
|
+
diagram_content = "```mermaid\n" + diagram_content
|
|
222
|
+
if not diagram_content.endswith("```"):
|
|
223
|
+
diagram_content = diagram_content + "\n```"
|
|
224
|
+
return diagram_content
|
|
225
|
+
|
|
226
|
+
def render_chat_sidebar(config=None):
|
|
227
|
+
"""Render the chat interface in the sidebar"""
|
|
228
|
+
# Add custom CSS for the diagram container
|
|
229
|
+
st.markdown("""
|
|
230
|
+
<style>
|
|
231
|
+
.diagram-container {
|
|
232
|
+
min-height: 400px;
|
|
233
|
+
max-height: 400px;
|
|
234
|
+
overflow-y: auto;
|
|
235
|
+
border: 1px solid #e0e0e0;
|
|
236
|
+
border-radius: 4px;
|
|
237
|
+
padding: 10px;
|
|
238
|
+
}
|
|
239
|
+
</style>
|
|
240
|
+
""", unsafe_allow_html=True)
|
|
241
|
+
|
|
242
|
+
with st.sidebar:
|
|
243
|
+
st.header("Chat Interface")
|
|
244
|
+
|
|
245
|
+
# Initialize agent if config provided
|
|
246
|
+
if config and st.session_state.agent_manager is None:
|
|
247
|
+
try:
|
|
248
|
+
st.session_state.agent_manager = AgentManager(config)
|
|
249
|
+
st.session_state.agent_manager.initialize()
|
|
250
|
+
except Exception as e:
|
|
251
|
+
st.error(f"Error initializing FEWS agent: {str(e)}")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Display chat messages
|
|
255
|
+
for idx, message in enumerate(st.session_state.messages):
|
|
256
|
+
with st.chat_message(message["role"]):
|
|
257
|
+
content = message["content"]
|
|
258
|
+
|
|
259
|
+
# Handle both string and dictionary content
|
|
260
|
+
if isinstance(content, str):
|
|
261
|
+
st.markdown(content)
|
|
262
|
+
else:
|
|
263
|
+
# Display text
|
|
264
|
+
if content.get("text"):
|
|
265
|
+
st.markdown(content["text"])
|
|
266
|
+
|
|
267
|
+
# Show table and diagram popovers if available
|
|
268
|
+
if content.get("table") or content.get("diagram"):
|
|
269
|
+
cols = st.columns(2)
|
|
270
|
+
|
|
271
|
+
# Table popover in first column if available
|
|
272
|
+
if content.get("table"):
|
|
273
|
+
with cols[0]:
|
|
274
|
+
with st.popover("📊 Table Details"):
|
|
275
|
+
st.markdown(content["table"])
|
|
276
|
+
|
|
277
|
+
# Diagram popover in second column if available
|
|
278
|
+
if content.get("diagram"):
|
|
279
|
+
with cols[1]:
|
|
280
|
+
with st.popover("📈 Diagram Details"):
|
|
281
|
+
# Format and display the mermaid diagram
|
|
282
|
+
formatted_diagram = format_mermaid_diagram(content["diagram"])
|
|
283
|
+
st_markdown(formatted_diagram, height=380)
|
|
284
|
+
|
|
285
|
+
# Process pending question if exists
|
|
286
|
+
if hasattr(st.session_state, 'pending_question') and st.session_state.pending_question:
|
|
287
|
+
question = st.session_state.pending_question
|
|
288
|
+
st.session_state.pending_question = None # Clear the pending question
|
|
289
|
+
|
|
290
|
+
# Get agent response if available
|
|
291
|
+
if st.session_state.agent_manager:
|
|
292
|
+
with st.spinner("Thinking..."):
|
|
293
|
+
try:
|
|
294
|
+
response = st.session_state.agent_manager.ask(question)
|
|
295
|
+
content = format_agent_response(response)
|
|
296
|
+
st.session_state.messages.append({
|
|
297
|
+
"role": "assistant",
|
|
298
|
+
"content": content
|
|
299
|
+
})
|
|
300
|
+
except Exception as e:
|
|
301
|
+
st.error(str(e))
|
|
302
|
+
st.session_state.messages.append({
|
|
303
|
+
"role": "assistant",
|
|
304
|
+
"content": {"text": f"Error: {str(e)}"}
|
|
305
|
+
})
|
|
306
|
+
st.rerun() # Update UI with the response
|
|
307
|
+
|
|
308
|
+
# Chat input
|
|
309
|
+
if prompt := st.chat_input("Ask about FEWS configuration..."):
|
|
310
|
+
# Add user message
|
|
311
|
+
st.session_state.messages.append({
|
|
312
|
+
"role": "user",
|
|
313
|
+
"content": prompt
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
# Store as pending question
|
|
317
|
+
st.session_state.pending_question = prompt
|
|
318
|
+
st.rerun()
|
|
319
|
+
|
|
320
|
+
# Cleanup button
|
|
321
|
+
if st.button("End Session", use_container_width=True):
|
|
322
|
+
if st.session_state.agent_manager:
|
|
323
|
+
st.session_state.agent_manager.close()
|
|
324
|
+
st.session_state.agent_manager = None
|
|
325
|
+
st.session_state.messages = []
|
|
326
|
+
st.session_state.pending_question = None
|
|
327
|
+
st.rerun()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from .sidebar_chat import add_to_chat
|
|
6
|
+
|
|
7
|
+
def load_workflow_data():
|
|
8
|
+
"""Load workflow data using the search_workflows function"""
|
|
9
|
+
try:
|
|
10
|
+
# Check if tools interface is available
|
|
11
|
+
if "tools_interface" not in st.session_state:
|
|
12
|
+
st.error("Database connection not available. Please check your configuration.")
|
|
13
|
+
return pd.DataFrame()
|
|
14
|
+
|
|
15
|
+
# Use the tools interface from session state
|
|
16
|
+
tools = st.session_state.tools_interface
|
|
17
|
+
|
|
18
|
+
# Use '%' as default pattern to get all workflows
|
|
19
|
+
results = tools.search_workflows(name_pattern='%')
|
|
20
|
+
|
|
21
|
+
if results and hasattr(results, 'results'):
|
|
22
|
+
data = []
|
|
23
|
+
for workflow in results.results:
|
|
24
|
+
data.append({
|
|
25
|
+
"ID": workflow.id, # Hidden column
|
|
26
|
+
"Workflow ID": workflow.wf_id,
|
|
27
|
+
"Beschrijving": workflow.desc,
|
|
28
|
+
"Module Config IDs": ', '.join(workflow.module_config_ids) if workflow.module_config_ids else 'Geen'
|
|
29
|
+
})
|
|
30
|
+
return pd.DataFrame(data)
|
|
31
|
+
return pd.DataFrame() # Return empty DataFrame if no results
|
|
32
|
+
except Exception as e:
|
|
33
|
+
st.error(f"Error loading workflows: {str(e)}")
|
|
34
|
+
return pd.DataFrame()
|
|
35
|
+
|
|
36
|
+
def render_explore_tab():
|
|
37
|
+
"""Render the explore tab content with workflow data"""
|
|
38
|
+
st.header("Explore Workflows")
|
|
39
|
+
|
|
40
|
+
# Load workflow data
|
|
41
|
+
df = load_workflow_data()
|
|
42
|
+
|
|
43
|
+
if df.empty:
|
|
44
|
+
st.warning("No workflows found or error loading workflows.")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Define which columns to display
|
|
48
|
+
display_columns = ["Workflow ID", "Beschrijving", "Module Config IDs"]
|
|
49
|
+
|
|
50
|
+
# Add search functionality
|
|
51
|
+
search_term = st.text_input("Search workflows", "")
|
|
52
|
+
|
|
53
|
+
# Filter dataframe based on search term
|
|
54
|
+
if search_term:
|
|
55
|
+
# Search in all columns, including hidden ones
|
|
56
|
+
mask = df.astype(str).apply(lambda x: x.str.contains(search_term, case=False)).any(axis=1)
|
|
57
|
+
filtered_df = df[mask]
|
|
58
|
+
else:
|
|
59
|
+
filtered_df = df
|
|
60
|
+
|
|
61
|
+
# Add header row
|
|
62
|
+
with st.container():
|
|
63
|
+
header_cols = st.columns([0.15] + [1] * len(display_columns))
|
|
64
|
+
header_cols[0].markdown("")
|
|
65
|
+
for i, col_name in enumerate(display_columns):
|
|
66
|
+
header_cols[i+1].markdown(f"**{col_name}**")
|
|
67
|
+
st.divider()
|
|
68
|
+
|
|
69
|
+
# Create a container for each row with hover effect
|
|
70
|
+
for idx, row in filtered_df.iterrows():
|
|
71
|
+
with st.container():
|
|
72
|
+
# Create columns only for displayed columns plus the chat button
|
|
73
|
+
cols = st.columns([0.15] + [1] * len(display_columns))
|
|
74
|
+
|
|
75
|
+
# Add to chat button
|
|
76
|
+
if cols[0].button("💬", key=f"btn_{idx}", use_container_width=True):
|
|
77
|
+
# Add the workflow type to the dictionary
|
|
78
|
+
workflow_data = row.to_dict() # This includes all columns, even hidden ones
|
|
79
|
+
workflow_data["type"] = "workflow"
|
|
80
|
+
add_to_chat(workflow_data)
|
|
81
|
+
|
|
82
|
+
# Display only the visible columns
|
|
83
|
+
for i, col_name in enumerate(display_columns, 1):
|
|
84
|
+
cols[i].write(row[col_name])
|
|
85
|
+
|
|
86
|
+
# Add a subtle divider
|
|
87
|
+
st.divider()
|
|
88
|
+
|
|
89
|
+
# Show result count
|
|
90
|
+
st.markdown(f"**{len(filtered_df)} workflows found**" +
|
|
91
|
+
(f" (filtered from {len(df)} total)" if search_term else ""))
|