Midas Deploy Bot commited on
Commit
df2c2e7
·
0 Parent(s):

Deploy: 8c89a8fbe906551039281c5c5a0089572c945b0b

Browse files
Files changed (3) hide show
  1. README.md +10 -0
  2. app.py +260 -0
  3. requirements.txt +2 -0
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Midas Frontend
3
+ emoji: 🎨
4
+ colorFrom: green
5
+ colorTo: yellow
6
+ sdk: streamlit
7
+ sdk_version: 1.35.0
8
+ app_file: app.py
9
+ ---
10
+ # Midas Protocol Frontend
app.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import os
4
+ import time
5
+ from typing import Optional, List, Dict
6
+ from dataclasses import dataclass, field
7
+ from huggingface_hub import HfApi
8
+
9
+ @dataclass
10
+ class ProviderConfig:
11
+ """
12
+ Defines the capabilities of a provider.
13
+ Future Proofing: Added 'models' list for granular selection.
14
+ """
15
+ key: str # Internal ID (groq, gemini)
16
+ display_name: str # UI Label
17
+ icon: str # UI Icon
18
+ default_model: str
19
+ available_models: List[str] = field(default_factory=list)
20
+
21
+ AVAILABLE_PROVIDERS = [
22
+ ProviderConfig(
23
+ key="groq",
24
+ display_name="Groq Cloud",
25
+ icon="⚡",
26
+ default_model="llama3-70b-8192",
27
+ available_models=["llama3-70b-8192", "mixtral-8x7b-32768"]
28
+ ),
29
+ ProviderConfig(
30
+ key="gemini",
31
+ display_name="Google Gemini",
32
+ icon="💎",
33
+ default_model="gemini-pro",
34
+ available_models=["gemini-pro", "gemini-1.5-flash"]
35
+ ),
36
+ ]
37
+
38
+ @dataclass
39
+ class AppSettings:
40
+ backend_url: str = os.getenv("BACKEND_URL", "http://localhost:7860/chat")
41
+ backend_repo_id: str = os.getenv("BACKEND_REPO_ID", "")
42
+ title: str = "Financial Multi-Agent System"
43
+
44
+ def ensure_backend_ready(settings: AppSettings):
45
+ # Skip if we've already verified the backend this session
46
+ if st.session_state.get("backend_ready", False):
47
+ return
48
+
49
+ health_url = settings.backend_url.rsplit('/', 1)[0] + "/docs"
50
+ hf_token = os.environ.get("HF_TOKEN")
51
+
52
+ with st.status("Verifying system health...", expanded=True) as status:
53
+ try:
54
+ # Step 1: Fast Ping
55
+ status.write("Pinging backend services...")
56
+ res = requests.get(health_url, timeout=5)
57
+ res.raise_for_status()
58
+
59
+ status.update(label="System is online and ready!", state="complete", expanded=False)
60
+ st.session_state.backend_ready = True
61
+ return
62
+
63
+ except requests.exceptions.RequestException:
64
+ status.write("Backend is unresponsive. Initiating auto-recovery...")
65
+
66
+ # Step 2: Restart Sequence
67
+ if not hf_token or not settings.backend_repo_id:
68
+ status.update(label="Auto-recovery unavailable (Missing HF configs).", state="error")
69
+ return
70
+
71
+ try:
72
+ status.write(f"Calling Hugging Face API to wake {settings.backend_repo_id}...")
73
+ api = HfApi()
74
+ api.restart_space(repo_id=settings.backend_repo_id, token=hf_token)
75
+ status.write("Container restart initiated.")
76
+ except Exception as e:
77
+ status.update(label=f"Failed to trigger restart: {e}", state="error")
78
+ return
79
+
80
+ # Step 3: Active Polling
81
+ status.write("Waiting for backend container to spin up (this may take 1-2 minutes)...")
82
+ for attempt in range(1, 25): # 24 attempts * 5 seconds = 2 minutes max
83
+ status.write(f"Polling attempt {attempt}/24...")
84
+ try:
85
+ res = requests.get(health_url, timeout=3)
86
+ if res.status_code == 200:
87
+ status.update(label="Auto-recovery successful! System is ready.", state="complete", expanded=False)
88
+ st.session_state.backend_ready = True
89
+ return
90
+ except requests.exceptions.RequestException:
91
+ pass # Expected while booting
92
+
93
+ time.sleep(5)
94
+
95
+ status.update(label="System recovery timed out. Please refresh the page.", state="error")
96
+
97
+ class SessionManager:
98
+ """
99
+ Encapsulates all Streamlit Session State logic.
100
+ """
101
+ def __init__(self):
102
+ if "messages" not in st.session_state: st.session_state.messages = []
103
+ if "pending_query" not in st.session_state: st.session_state.pending_query = None
104
+
105
+ @property
106
+ def messages(self): return st.session_state.messages
107
+
108
+ def add_message(self, role: str, content: str):
109
+ st.session_state.messages.append({"role": role, "content": content})
110
+
111
+ def get_api_key(self, provider_key: str) -> Optional[str]:
112
+ return st.session_state.get(f"key_{provider_key}", None)
113
+
114
+ def set_api_key(self, provider_key: str, key_value: str):
115
+ st.session_state[f"key_{provider_key}"] = key_value
116
+
117
+ def set_pending_query(self, query: str):
118
+ st.session_state.pending_query = query
119
+
120
+ def pop_pending_query(self) -> Optional[str]:
121
+ q = st.session_state.pending_query
122
+ st.session_state.pending_query = None
123
+ return q
124
+
125
+ class APIClient:
126
+ def __init__(self, base_url: str):
127
+ self.base_url = base_url
128
+
129
+ def send_chat(self, query: str, provider: str, api_key: Optional[str]) -> Dict:
130
+ try:
131
+ # Future Proofing: Sending 'model' here requires updating backend first
132
+ payload = {
133
+ "query": query,
134
+ "provider": provider,
135
+ "api_key": api_key
136
+ }
137
+ res = requests.post(self.base_url, json=payload, timeout=120)
138
+ res.raise_for_status()
139
+ return res.json()
140
+ except Exception as e:
141
+ # Return a consistent error structure
142
+ return {"success": False, "error_type": "client_error", "message": str(e)}
143
+
144
+ class SidebarComponent:
145
+ def render(self, providers: List[ProviderConfig]) -> tuple[Optional[str], ProviderConfig]:
146
+ with st.sidebar:
147
+ st.header("⚙️ Configuration")
148
+
149
+ # Dynamic Provider Selection
150
+ # This loops through config, making it extensible
151
+ provider_names = [p.display_name for p in providers]
152
+ selected_name = st.selectbox("Select Provider", provider_names)
153
+
154
+ # Find the config object for the selected name
155
+ active_config = next(p for p in providers if p.display_name == selected_name)
156
+
157
+ # Dynamic Model Selection (Future Proofing)
158
+ st.selectbox(f"{active_config.icon} Model", active_config.available_models)
159
+
160
+ # API Key Input
161
+ api_key = st.text_input(
162
+ f"{active_config.display_name} Key",
163
+ type="password",
164
+ help=f"Enter key for {active_config.key}"
165
+ )
166
+
167
+ st.divider()
168
+
169
+ # Quick Actions (Could also be data-driven in future)
170
+ st.caption("Quick Tests")
171
+ if st.button("💰 TSLA Price Check"):
172
+ st.session_state.pending_query = "Check Tesla price"
173
+
174
+ return api_key, active_config
175
+
176
+ class ChatComponent:
177
+ def render_history(self, messages):
178
+ for msg in messages:
179
+ with st.chat_message(msg["role"]):
180
+ st.markdown(msg["content"])
181
+
182
+ def render_recovery_form(self, provider_key: str, message: str) -> Optional[str]:
183
+ """
184
+ Renders the 'Needs Key' form. Returns the new key if submitted.
185
+ """
186
+ with st.container():
187
+ st.warning(f"⚠️ {message}")
188
+ with st.form(f"fix_{provider_key}"):
189
+ val = st.text_input(f"Enter {provider_key.upper()} Key:", type="password")
190
+ if st.form_submit_button("Retry"):
191
+ return val
192
+ return None
193
+
194
+ class Application:
195
+ def __init__(self):
196
+ self.settings = AppSettings()
197
+ self.session = SessionManager()
198
+ self.client = APIClient(self.settings.backend_url)
199
+ self.sidebar = SidebarComponent()
200
+ self.chat = ChatComponent()
201
+
202
+ def run(self):
203
+ st.set_page_config(page_title=self.settings.title, layout="wide")
204
+ st.title(self.settings.title)
205
+
206
+ ensure_backend_ready(self.settings)
207
+
208
+ # 1. Sidebar
209
+ user_key, config = self.sidebar.render(AVAILABLE_PROVIDERS)
210
+ if user_key:
211
+ self.session.set_api_key(config.key, user_key)
212
+
213
+ # 2. History
214
+ self.chat.render_history(self.session.messages)
215
+
216
+ # 3. Input Handling
217
+ query = self.session.pop_pending_query() or st.chat_input("Input query...")
218
+
219
+ if query:
220
+ self.process_query(query, config)
221
+
222
+ def process_query(self, query: str, config: ProviderConfig):
223
+ # Optimistic UI Update
224
+ self.session.add_message("user", query)
225
+ with st.chat_message("user"): st.markdown(query)
226
+
227
+ with st.chat_message("assistant"):
228
+ placeholder = st.empty()
229
+ placeholder.markdown("⏳ *Thinking...*")
230
+
231
+ # Resolve Key: User Input > Session Store
232
+ # This allows the sidebar input to override everything
233
+ final_key = self.session.get_api_key(config.key)
234
+
235
+ # API Call
236
+ response = self.client.send_chat(query, config.key, final_key)
237
+
238
+ # Handle Response
239
+ if response.get("success"):
240
+ content = response["response"]
241
+ placeholder.markdown(content)
242
+ self.session.add_message("assistant", content)
243
+
244
+ elif response.get("error_type") == "needs_key":
245
+ placeholder.empty()
246
+ new_key = self.chat.render_recovery_form(
247
+ response["required_provider"],
248
+ response["message"]
249
+ )
250
+ if new_key:
251
+ # Save and Retry
252
+ self.session.set_api_key(response["required_provider"], new_key)
253
+ self.session.set_pending_query(query)
254
+ st.rerun()
255
+ else:
256
+ placeholder.error(f"❌ {response.get('message')}")
257
+
258
+ if __name__ == "__main__":
259
+ app = Application()
260
+ app.run()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit
2
+ requests