Spaces:
Running on Zero
Running on Zero
Upload community_contributions/chrys/app.py with huggingface_hub
Browse files
community_contributions/chrys/app.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio UI for ARIA: agent-colored log stream and results table."""
|
| 2 |
+
import html
|
| 3 |
+
import logging
|
| 4 |
+
import queue
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
# Ensure chrys is on path
|
| 10 |
+
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
+
if _this_dir not in sys.path:
|
| 12 |
+
sys.path.insert(0, _this_dir)
|
| 13 |
+
|
| 14 |
+
import gradio as gr
|
| 15 |
+
from dotenv import load_dotenv
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
from orchestrator import run_pipeline
|
| 19 |
+
from models import DecisionRecord
|
| 20 |
+
|
| 21 |
+
# Agent name + color for UI (logger name -> (display name, hex color))
|
| 22 |
+
AGENT_COLORS = {
|
| 23 |
+
"aria.data_fetcher": ("DATA_FETCHER", "#3498db"),
|
| 24 |
+
"aria.tech_analyst": ("TECH_ANALYST", "#27ae60"),
|
| 25 |
+
"aria.sentiment_agent": ("SENTIMENT_AGENT", "#f39c12"),
|
| 26 |
+
"aria.decision_agent": ("DECISION_AGENT", "#9b59b6"),
|
| 27 |
+
"aria.notifier": ("NOTIFIER", "#1abc9c"),
|
| 28 |
+
}
|
| 29 |
+
DEFAULT_COLOR = "#bdc3c7"
|
| 30 |
+
MAX_LOG_LINES = 150
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class AgentQueueHandler(logging.Handler):
|
| 34 |
+
"""Put (logger_name, formatted_message) into queue for colored UI."""
|
| 35 |
+
def __init__(self, log_queue: queue.Queue):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.log_queue = log_queue
|
| 38 |
+
|
| 39 |
+
def emit(self, record: logging.LogRecord):
|
| 40 |
+
try:
|
| 41 |
+
msg = self.format(record)
|
| 42 |
+
self.log_queue.put((record.name, msg))
|
| 43 |
+
except Exception:
|
| 44 |
+
self.handleError(record)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def agent_line_to_html(logger_name: str, message: str) -> str:
|
| 48 |
+
label, color = AGENT_COLORS.get(logger_name, ("ARIA", DEFAULT_COLOR))
|
| 49 |
+
safe_msg = html.escape(message)
|
| 50 |
+
return f'<span style="color:{color};font-weight:600">[{label}]</span> {safe_msg}'
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def html_for_log(log_lines: list) -> str:
|
| 54 |
+
recent = log_lines[-MAX_LOG_LINES:]
|
| 55 |
+
content = "<br>".join(recent)
|
| 56 |
+
return f"""
|
| 57 |
+
<div style="height:420px;overflow-y:auto;border:1px solid #444;background:#1e1e2e;padding:12px;font-family:monospace;font-size:13px;color:#cdd6f4;">
|
| 58 |
+
{content}
|
| 59 |
+
</div>
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def setup_aria_logging(log_queue: queue.Queue) -> None:
|
| 64 |
+
root = logging.getLogger()
|
| 65 |
+
handler = AgentQueueHandler(log_queue)
|
| 66 |
+
handler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", datefmt="%H:%M:%S"))
|
| 67 |
+
root.addHandler(handler)
|
| 68 |
+
root.setLevel(logging.INFO)
|
| 69 |
+
for name in AGENT_COLORS:
|
| 70 |
+
logging.getLogger(name).setLevel(logging.INFO)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def table_for(records: list[DecisionRecord]) -> list[list]:
|
| 74 |
+
if not records:
|
| 75 |
+
return []
|
| 76 |
+
return [
|
| 77 |
+
[r.asset, str(r.tech_score), r.sentiment, f"{r.final_score:.1f}", r.decision, r.skip_reason or ""]
|
| 78 |
+
for r in records
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def run_pipeline_with_logging(log_queue: queue.Queue, result_queue: queue.Queue) -> None:
|
| 83 |
+
try:
|
| 84 |
+
records, _ = run_pipeline()
|
| 85 |
+
result_queue.put(records)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
logging.exception("Pipeline failed")
|
| 88 |
+
result_queue.put([])
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def run_clicked(log_state):
|
| 92 |
+
log_queue = queue.Queue()
|
| 93 |
+
result_queue = queue.Queue()
|
| 94 |
+
root = logging.getLogger()
|
| 95 |
+
for h in root.handlers[:]:
|
| 96 |
+
if isinstance(h, AgentQueueHandler):
|
| 97 |
+
root.removeHandler(h)
|
| 98 |
+
setup_aria_logging(log_queue)
|
| 99 |
+
log_lines = []
|
| 100 |
+
thread = __import__("threading").Thread(
|
| 101 |
+
target=run_pipeline_with_logging,
|
| 102 |
+
args=(log_queue, result_queue),
|
| 103 |
+
daemon=True,
|
| 104 |
+
)
|
| 105 |
+
thread.start()
|
| 106 |
+
table = []
|
| 107 |
+
while True:
|
| 108 |
+
while True:
|
| 109 |
+
try:
|
| 110 |
+
logger_name, msg = log_queue.get_nowait()
|
| 111 |
+
log_lines.append(agent_line_to_html(logger_name, msg))
|
| 112 |
+
except queue.Empty:
|
| 113 |
+
break
|
| 114 |
+
try:
|
| 115 |
+
records = result_queue.get_nowait()
|
| 116 |
+
table = table_for(records)
|
| 117 |
+
break
|
| 118 |
+
except queue.Empty:
|
| 119 |
+
pass
|
| 120 |
+
yield log_lines, html_for_log(log_lines), table
|
| 121 |
+
time.sleep(0.08)
|
| 122 |
+
if not thread.is_alive():
|
| 123 |
+
try:
|
| 124 |
+
records = result_queue.get_nowait()
|
| 125 |
+
table = table_for(records)
|
| 126 |
+
except queue.Empty:
|
| 127 |
+
table = []
|
| 128 |
+
break
|
| 129 |
+
yield log_lines, html_for_log(log_lines), table
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def build_ui():
|
| 133 |
+
with gr.Blocks(title="ARIA — Market Intelligence", theme=gr.themes.Soft(), css="""
|
| 134 |
+
.log-panel { font-family: ui-monospace, monospace; }
|
| 135 |
+
""") as ui:
|
| 136 |
+
gr.Markdown("# ARIA — Automated Real-time Investment Alert Agent")
|
| 137 |
+
gr.Markdown("Multi-agent pipeline: Data → Tech Analysis → Sentiment → Decision → Pushover alerts.")
|
| 138 |
+
log_state = gr.State([])
|
| 139 |
+
with gr.Row():
|
| 140 |
+
run_btn = gr.Button("Run pipeline", variant="primary")
|
| 141 |
+
with gr.Row():
|
| 142 |
+
log_html = gr.HTML(value=html_for_log([]), label="Agent log")
|
| 143 |
+
with gr.Row():
|
| 144 |
+
results_df = gr.Dataframe(
|
| 145 |
+
headers=["Asset", "Tech score", "Sentiment", "Final score", "Decision", "Skip reason"],
|
| 146 |
+
datatype=["str", "str", "str", "str", "str", "str"],
|
| 147 |
+
label="Last run results",
|
| 148 |
+
)
|
| 149 |
+
run_btn.click(
|
| 150 |
+
fn=run_clicked,
|
| 151 |
+
inputs=[log_state],
|
| 152 |
+
outputs=[log_state, log_html, results_df],
|
| 153 |
+
)
|
| 154 |
+
return ui
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
ui = build_ui()
|
| 159 |
+
ui.launch(share=False, inbrowser=True)
|