Spaces:
Sleeping
Sleeping
RemanenetSpy commited on
Commit ·
7fc8c13
1
Parent(s): d8952c8
Refactor: Implement LiteLLM Mixture of Agents Orchestration
Browse files- .env.example +4 -0
- agent/nodes.py +4 -9
- chronos_core/llm_router.py +64 -0
- chronos_core/svo_parser.py +32 -53
- dashboard/app.py +3 -3
- requirements.txt +1 -0
- scratch_remove.py +35 -0
.env.example
CHANGED
|
@@ -5,6 +5,10 @@
|
|
| 5 |
# Get your free key at: https://console.groq.com
|
| 6 |
GROQ_API_KEY=gsk_your_key_here
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
# Razorpay (for premium tier billing)
|
| 9 |
RAZORPAY_KEY_ID=rzp_test_...
|
| 10 |
RAZORPAY_KEY_SECRET=secret_...
|
|
|
|
| 5 |
# Get your free key at: https://console.groq.com
|
| 6 |
GROQ_API_KEY=gsk_your_key_here
|
| 7 |
|
| 8 |
+
# Cerebras Inference API (free 1M tokens/day)
|
| 9 |
+
# Get your free key at: https://inference.cerebras.ai
|
| 10 |
+
CEREBRAS_API_KEY=cbr_your_key_here
|
| 11 |
+
|
| 12 |
# Razorpay (for premium tier billing)
|
| 13 |
RAZORPAY_KEY_ID=rzp_test_...
|
| 14 |
RAZORPAY_KEY_SECRET=secret_...
|
agent/nodes.py
CHANGED
|
@@ -114,16 +114,11 @@ async def call_model_node(state: dict) -> dict:
|
|
| 114 |
if not isinstance(msg, SystemMessage):
|
| 115 |
augmented.append(msg)
|
| 116 |
|
| 117 |
-
# Get the LLM
|
| 118 |
try:
|
| 119 |
-
from
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
llm = ChatGroq(
|
| 123 |
-
model="llama-3.3-70b-versatile",
|
| 124 |
-
api_key=os.getenv("GROQ_API_KEY", ""),
|
| 125 |
-
temperature=0.7,
|
| 126 |
-
)
|
| 127 |
|
| 128 |
# Bind tools so the agent can interact with SaaS connectors
|
| 129 |
from agent.tools import CHRONOS_TOOLS
|
|
|
|
| 114 |
if not isinstance(msg, SystemMessage):
|
| 115 |
augmented.append(msg)
|
| 116 |
|
| 117 |
+
# Get the LLM from the Mixture of Agents Router (Heavy Pipeline)
|
| 118 |
try:
|
| 119 |
+
from chronos_core.llm_router import get_heavy_pipeline
|
| 120 |
+
|
| 121 |
+
llm = get_heavy_pipeline()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
# Bind tools so the agent can interact with SaaS connectors
|
| 124 |
from agent.tools import CHRONOS_TOOLS
|
chronos_core/llm_router.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chronos OS — Multi-Model Mixture of Agents Router
|
| 3 |
+
===================================================
|
| 4 |
+
Intelligently routes tasks to different LLMs to aggregate free-tier API limits.
|
| 5 |
+
Falls back safely if rate limits (429) or downtimes occur.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
from langchain_community.chat_models import ChatLiteLLM
|
| 13 |
+
from langchain_core.language_models.chat_models import BaseChatModel
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger("chronos.llm_router")
|
| 16 |
+
|
| 17 |
+
def get_fast_pipeline_kwargs() -> dict[str, Any]:
|
| 18 |
+
"""
|
| 19 |
+
Routing configuration for the Fast Pipeline (Memory Extraction).
|
| 20 |
+
Primary: Cerebras Llama 3.1 8B (Massive limit, extreme speed)
|
| 21 |
+
Fallback: Groq Llama 3.1 8B Instant
|
| 22 |
+
"""
|
| 23 |
+
has_cerebras = bool(os.getenv("CEREBRAS_API_KEY"))
|
| 24 |
+
has_groq = bool(os.getenv("GROQ_API_KEY"))
|
| 25 |
+
|
| 26 |
+
if has_cerebras and has_groq:
|
| 27 |
+
return {
|
| 28 |
+
"model": "cerebras/llama3.1-8b",
|
| 29 |
+
"fallbacks": [{"model": "groq/llama-3.1-8b-instant"}]
|
| 30 |
+
}
|
| 31 |
+
elif has_cerebras:
|
| 32 |
+
return {"model": "cerebras/llama3.1-8b"}
|
| 33 |
+
else:
|
| 34 |
+
# Default to Groq if no Cerebras key is found
|
| 35 |
+
return {"model": "groq/llama-3.1-8b-instant"}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_heavy_pipeline() -> BaseChatModel:
|
| 39 |
+
"""
|
| 40 |
+
Routing configuration for the Heavy Pipeline (Agentic Reasoning).
|
| 41 |
+
Primary: Cerebras Qwen 3 235B (Elite logic and deep reasoning)
|
| 42 |
+
Fallback: Groq Llama 3.3 70B
|
| 43 |
+
"""
|
| 44 |
+
has_cerebras = bool(os.getenv("CEREBRAS_API_KEY"))
|
| 45 |
+
has_groq = bool(os.getenv("GROQ_API_KEY"))
|
| 46 |
+
|
| 47 |
+
# Initialize the robust Groq fallback (always available if .env set)
|
| 48 |
+
# Using litellm via langchain
|
| 49 |
+
groq_llm = ChatLiteLLM(
|
| 50 |
+
model="groq/llama-3.3-70b-versatile",
|
| 51 |
+
temperature=0.4
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
if has_cerebras:
|
| 55 |
+
cerebras_llm = ChatLiteLLM(
|
| 56 |
+
model="cerebras/qwen3-235b",
|
| 57 |
+
temperature=0.4
|
| 58 |
+
)
|
| 59 |
+
if has_groq:
|
| 60 |
+
# Langchain native fallback mechanism
|
| 61 |
+
return cerebras_llm.with_fallbacks([groq_llm])
|
| 62 |
+
return cerebras_llm
|
| 63 |
+
|
| 64 |
+
return groq_llm
|
chronos_core/svo_parser.py
CHANGED
|
@@ -128,36 +128,14 @@ class SVOParser:
|
|
| 128 |
"""
|
| 129 |
Extracts Subject-Verb-Object tuples from raw text.
|
| 130 |
|
| 131 |
-
Primary:
|
| 132 |
-
Fallback: Regex patterns (when LLM fails or
|
| 133 |
"""
|
| 134 |
|
| 135 |
-
def __init__(
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
):
|
| 140 |
-
self.api_key = api_key or os.getenv("GROQ_API_KEY", "")
|
| 141 |
-
self.model = model
|
| 142 |
-
self._client = None
|
| 143 |
-
|
| 144 |
-
if self.api_key:
|
| 145 |
-
try:
|
| 146 |
-
from groq import Groq
|
| 147 |
-
self._client = Groq(api_key=self.api_key)
|
| 148 |
-
logger.info(f"Groq SVO parser initialized with model: {self.model}")
|
| 149 |
-
except ImportError:
|
| 150 |
-
logger.warning(
|
| 151 |
-
"groq not installed. Using regex fallback. "
|
| 152 |
-
"Install with: pip install groq"
|
| 153 |
-
)
|
| 154 |
-
except Exception as e:
|
| 155 |
-
logger.warning(f"Failed to init Groq client: {e}. Using regex fallback.")
|
| 156 |
-
else:
|
| 157 |
-
logger.warning(
|
| 158 |
-
"No GROQ_API_KEY set. SVO parser will use regex fallback only. "
|
| 159 |
-
"Get a free key at https://console.groq.com"
|
| 160 |
-
)
|
| 161 |
|
| 162 |
async def parse(
|
| 163 |
self,
|
|
@@ -166,51 +144,52 @@ class SVOParser:
|
|
| 166 |
) -> list[SVOTuple]:
|
| 167 |
"""
|
| 168 |
Extract SVO tuples from text.
|
| 169 |
-
Tries
|
| 170 |
"""
|
| 171 |
if not text or not text.strip():
|
| 172 |
return []
|
| 173 |
|
| 174 |
ts = timestamp or datetime.utcnow()
|
| 175 |
|
| 176 |
-
# Try LLM extraction
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
logger.warning(f"Groq SVO extraction failed: {e}. Falling back to regex.")
|
| 182 |
|
| 183 |
# Regex fallback
|
| 184 |
return _regex_fallback(text, ts)
|
| 185 |
|
| 186 |
-
async def
|
| 187 |
self,
|
| 188 |
text: str,
|
| 189 |
timestamp: datetime,
|
| 190 |
) -> list[SVOTuple]:
|
| 191 |
-
"""Call
|
| 192 |
-
import
|
|
|
|
| 193 |
|
| 194 |
prompt = SVO_EXTRACTION_PROMPT.format(
|
| 195 |
text=text,
|
| 196 |
current_time=timestamp.isoformat(),
|
| 197 |
)
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
| 214 |
|
| 215 |
raw = response.choices[0].message.content.strip()
|
| 216 |
|
|
|
|
| 128 |
"""
|
| 129 |
Extracts Subject-Verb-Object tuples from raw text.
|
| 130 |
|
| 131 |
+
Primary: High Speed Model via LiteLLM Router.
|
| 132 |
+
Fallback: Regex patterns (when LLM fails or limits exhausted)
|
| 133 |
"""
|
| 134 |
|
| 135 |
+
def __init__(self):
|
| 136 |
+
from .llm_router import get_fast_pipeline_kwargs
|
| 137 |
+
self._llm_kwargs = get_fast_pipeline_kwargs()
|
| 138 |
+
logger.info(f"SVO parser initialized with fast pipeline: {self._llm_kwargs.get('model')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
async def parse(
|
| 141 |
self,
|
|
|
|
| 144 |
) -> list[SVOTuple]:
|
| 145 |
"""
|
| 146 |
Extract SVO tuples from text.
|
| 147 |
+
Tries Litellm Fast Pipeline first, falls back to regex.
|
| 148 |
"""
|
| 149 |
if not text or not text.strip():
|
| 150 |
return []
|
| 151 |
|
| 152 |
ts = timestamp or datetime.utcnow()
|
| 153 |
|
| 154 |
+
# Try LLM extraction via LiteLLM
|
| 155 |
+
try:
|
| 156 |
+
return await self._parse_with_litellm(text, ts)
|
| 157 |
+
except Exception as e:
|
| 158 |
+
logger.warning(f"Fast Pipeline SVO extraction failed: {e}. Falling back to regex.")
|
|
|
|
| 159 |
|
| 160 |
# Regex fallback
|
| 161 |
return _regex_fallback(text, ts)
|
| 162 |
|
| 163 |
+
async def _parse_with_litellm(
|
| 164 |
self,
|
| 165 |
text: str,
|
| 166 |
timestamp: datetime,
|
| 167 |
) -> list[SVOTuple]:
|
| 168 |
+
"""Call LiteLLM unified completion to extract SVO tuples."""
|
| 169 |
+
import litellm
|
| 170 |
+
from .llm_router import get_fast_pipeline_kwargs
|
| 171 |
|
| 172 |
prompt = SVO_EXTRACTION_PROMPT.format(
|
| 173 |
text=text,
|
| 174 |
current_time=timestamp.isoformat(),
|
| 175 |
)
|
| 176 |
|
| 177 |
+
kwargs = get_fast_pipeline_kwargs()
|
| 178 |
+
kwargs["messages"] = [
|
| 179 |
+
{
|
| 180 |
+
"role": "system",
|
| 181 |
+
"content": "You are a precise JSON event extractor. Output ONLY valid JSON arrays.",
|
| 182 |
+
},
|
| 183 |
+
{"role": "user", "content": prompt},
|
| 184 |
+
]
|
| 185 |
+
kwargs["temperature"] = 0.1
|
| 186 |
+
kwargs["max_tokens"] = 2000
|
| 187 |
+
# Not all fallback models gracefully support strict json format enforcement in litellm uniformly if the provider doesn't support it.
|
| 188 |
+
# But we will use it for safety.
|
| 189 |
+
kwargs["response_format"] = {"type": "json_object"}
|
| 190 |
+
|
| 191 |
+
# LiteLLM acompletion directly leverages the fallback arrays natively!
|
| 192 |
+
response = await litellm.acompletion(**kwargs)
|
| 193 |
|
| 194 |
raw = response.choices[0].message.content.strip()
|
| 195 |
|
dashboard/app.py
CHANGED
|
@@ -560,7 +560,7 @@ if page == "⟡ Overview":
|
|
| 560 |
with col4:
|
| 561 |
st.markdown("""
|
| 562 |
<div class="metric-card">
|
| 563 |
-
<div class="metric-value" style="font-size:1.
|
| 564 |
<div class="metric-label">AI Engine</div>
|
| 565 |
</div>
|
| 566 |
""", unsafe_allow_html=True)
|
|
@@ -613,7 +613,7 @@ elif page == "⭳ Ingest Events":
|
|
| 613 |
label_visibility="collapsed",
|
| 614 |
)
|
| 615 |
|
| 616 |
-
parse_svo = st.checkbox("✧ Enable
|
| 617 |
|
| 618 |
if st.button("⭳ Ingest Into Memory") and stored_key and events_text.strip():
|
| 619 |
lines = [l.strip() for l in events_text.strip().split("\n") if l.strip()]
|
|
@@ -623,7 +623,7 @@ elif page == "⭳ Ingest Events":
|
|
| 623 |
"parse_svo": parse_svo,
|
| 624 |
}
|
| 625 |
|
| 626 |
-
with st.spinner("Llama
|
| 627 |
result = api_call("POST", "/ingest", stored_key, json=payload)
|
| 628 |
|
| 629 |
if "error" not in result:
|
|
|
|
| 560 |
with col4:
|
| 561 |
st.markdown("""
|
| 562 |
<div class="metric-card">
|
| 563 |
+
<div class="metric-value" style="font-size:1.4rem; padding-top: 0.3rem;">Qwen 3 + Llama Mixture</div>
|
| 564 |
<div class="metric-label">AI Engine</div>
|
| 565 |
</div>
|
| 566 |
""", unsafe_allow_html=True)
|
|
|
|
| 613 |
label_visibility="collapsed",
|
| 614 |
)
|
| 615 |
|
| 616 |
+
parse_svo = st.checkbox("✧ Enable Fast SVO Parsing (Llama Mixture)", value=True)
|
| 617 |
|
| 618 |
if st.button("⭳ Ingest Into Memory") and stored_key and events_text.strip():
|
| 619 |
lines = [l.strip() for l in events_text.strip().split("\n") if l.strip()]
|
|
|
|
| 623 |
"parse_svo": parse_svo,
|
| 624 |
}
|
| 625 |
|
| 626 |
+
with st.spinner("Llama Engine is extracting temporal events..."):
|
| 627 |
result = api_call("POST", "/ingest", stored_key, json=payload)
|
| 628 |
|
| 629 |
if "error" not in result:
|
requirements.txt
CHANGED
|
@@ -13,3 +13,4 @@ razorpay>=1.4.0
|
|
| 13 |
python-dotenv>=1.0.0
|
| 14 |
httpx>=0.28.0
|
| 15 |
sentence-transformers>=3.0.0
|
|
|
|
|
|
| 13 |
python-dotenv>=1.0.0
|
| 14 |
httpx>=0.28.0
|
| 15 |
sentence-transformers>=3.0.0
|
| 16 |
+
langchain-community>=0.3.0
|
scratch_remove.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
def remove_emojis():
|
| 4 |
+
filepath = "dashboard/app.py"
|
| 5 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 6 |
+
content = f.read()
|
| 7 |
+
|
| 8 |
+
# Define exact replacements
|
| 9 |
+
replacements = {
|
| 10 |
+
"🔑": "⚷",
|
| 11 |
+
"📥": "⭳",
|
| 12 |
+
"🔍": "⚲",
|
| 13 |
+
"🤖": "✦",
|
| 14 |
+
"🔗": "⚙",
|
| 15 |
+
"📊": "▤",
|
| 16 |
+
"✅": "✓",
|
| 17 |
+
"⚠️": "⚠",
|
| 18 |
+
"✨": "✧",
|
| 19 |
+
"🕰️": "⧗",
|
| 20 |
+
"🏠": "⌂"
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
for emoji, symbol in replacements.items():
|
| 24 |
+
content = content.replace(emoji, symbol)
|
| 25 |
+
|
| 26 |
+
# Some old ones might still have the space, or might need removal.
|
| 27 |
+
# The symbols above are very elegant, but if the user wants them completely gone:
|
| 28 |
+
# "remove evry possible emoji... replace them with something better and adjust that name also"
|
| 29 |
+
|
| 30 |
+
with open(filepath, "w", encoding="utf-8") as f:
|
| 31 |
+
f.write(content)
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
remove_emojis()
|
| 35 |
+
print("Emojis replaced successfully.")
|