Make SIF agent workflows non-blocking and guard SSE hangs
This commit is contained in:
@@ -158,6 +158,16 @@ class SIFBaseAgent(BaseALwrityAgent):
|
|||||||
if kwargs:
|
if kwargs:
|
||||||
logger.debug(f"[{self.__class__.__name__}] Parameters: {kwargs}")
|
logger.debug(f"[{self.__class__.__name__}] Parameters: {kwargs}")
|
||||||
|
|
||||||
|
async def _ensure_intelligence_ready(self) -> bool:
|
||||||
|
"""Ensure txtai intelligence service is initialized without blocking the event loop."""
|
||||||
|
try:
|
||||||
|
await self.intelligence._ensure_initialized_async()
|
||||||
|
except Exception as init_err:
|
||||||
|
logger.warning(f"[{self.__class__.__name__}] Intelligence initialization failed: {init_err}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return bool(getattr(self.intelligence, "_initialized", False) and self.intelligence.embeddings)
|
||||||
|
|
||||||
def _create_txtai_agent(self):
|
def _create_txtai_agent(self):
|
||||||
"""
|
"""
|
||||||
SIF agents primarily use the intelligence service directly, but we can expose
|
SIF agents primarily use the intelligence service directly, but we can expose
|
||||||
@@ -186,11 +196,7 @@ class StrategyArchitectAgent(SIFBaseAgent):
|
|||||||
self._log_agent_operation("Discovering content pillars")
|
self._log_agent_operation("Discovering content pillars")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if intelligence service is initialized
|
# Let intelligence service perform lazy async initialization internally.
|
||||||
if not self.intelligence.is_initialized():
|
|
||||||
logger.error(f"[{self.__class__.__name__}] Intelligence service not initialized")
|
|
||||||
return []
|
|
||||||
|
|
||||||
clusters = await self.intelligence.cluster(min_score=0.6)
|
clusters = await self.intelligence.cluster(min_score=0.6)
|
||||||
|
|
||||||
if not clusters:
|
if not clusters:
|
||||||
@@ -370,14 +376,14 @@ class StrategyArchitectAgent(SIFBaseAgent):
|
|||||||
|
|
||||||
async def _fetch_index_documents(self) -> List[Dict[str, Any]]:
|
async def _fetch_index_documents(self) -> List[Dict[str, Any]]:
|
||||||
"""Fetch indexed documents and normalize metadata from txtai result objects."""
|
"""Fetch indexed documents and normalize metadata from txtai result objects."""
|
||||||
if not self.intelligence.is_initialized() or not self.intelligence.embeddings:
|
if not await self._ensure_intelligence_ready():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
embeddings = self.intelligence.embeddings
|
embeddings = self.intelligence.embeddings
|
||||||
limit = 0
|
limit = 0
|
||||||
if hasattr(embeddings, "count"):
|
if hasattr(embeddings, "count"):
|
||||||
try:
|
try:
|
||||||
limit = int(embeddings.count())
|
limit = int(await asyncio.to_thread(embeddings.count))
|
||||||
except Exception:
|
except Exception:
|
||||||
limit = 0
|
limit = 0
|
||||||
|
|
||||||
@@ -394,7 +400,7 @@ class StrategyArchitectAgent(SIFBaseAgent):
|
|||||||
for query in candidate_queries:
|
for query in candidate_queries:
|
||||||
try:
|
try:
|
||||||
query_limit = limit if query.startswith("select") and limit > 0 else max(10, limit or 50)
|
query_limit = limit if query.startswith("select") and limit > 0 else max(10, limit or 50)
|
||||||
rows = embeddings.search(query, limit=query_limit)
|
rows = await asyncio.to_thread(lambda: embeddings.search(query, limit=query_limit))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -565,7 +571,7 @@ class ContentGuardianAgent(SIFBaseAgent):
|
|||||||
self._log_agent_operation("Checking for semantic cannibalization", draft_length=len(new_draft))
|
self._log_agent_operation("Checking for semantic cannibalization", draft_length=len(new_draft))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.intelligence.is_initialized():
|
if not await self._ensure_intelligence_ready():
|
||||||
logger.error(f"[{self.__class__.__name__}] Intelligence service not initialized")
|
logger.error(f"[{self.__class__.__name__}] Intelligence service not initialized")
|
||||||
return {"warning": False, "error": "Service not initialized"}
|
return {"warning": False, "error": "Service not initialized"}
|
||||||
|
|
||||||
@@ -796,7 +802,7 @@ class LinkGraphAgent(SIFBaseAgent):
|
|||||||
self._log_agent_operation("Suggesting internal links", draft_length=len(draft))
|
self._log_agent_operation("Suggesting internal links", draft_length=len(draft))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.intelligence.is_initialized():
|
if not await self._ensure_intelligence_ready():
|
||||||
logger.error(f"[{self.__class__.__name__}] Intelligence service not initialized")
|
logger.error(f"[{self.__class__.__name__}] Intelligence service not initialized")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -876,7 +882,7 @@ class LinkGraphAgent(SIFBaseAgent):
|
|||||||
self._log_agent_operation("Building semantic link graph")
|
self._log_agent_operation("Building semantic link graph")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.intelligence.is_initialized():
|
if not await self._ensure_intelligence_ready():
|
||||||
return {"error": "Intelligence service not initialized"}
|
return {"error": "Intelligence service not initialized"}
|
||||||
|
|
||||||
# This is a resource-intensive operation in a real vector DB.
|
# This is a resource-intensive operation in a real vector DB.
|
||||||
@@ -1002,7 +1008,7 @@ class CitationExpert(SIFBaseAgent):
|
|||||||
self._log_agent_operation("Finding citations", topic=topic)
|
self._log_agent_operation("Finding citations", topic=topic)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.intelligence.is_initialized():
|
if not await self._ensure_intelligence_ready():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Search for highly relevant content
|
# Search for highly relevant content
|
||||||
|
|||||||
@@ -222,32 +222,15 @@ class TxtaiIntelligenceService:
|
|||||||
|
|
||||||
async def index_content(self, items: List[Tuple[str, str, Dict[str, Any]]]):
|
async def index_content(self, items: List[Tuple[str, str, Dict[str, Any]]]):
|
||||||
"""
|
"""
|
||||||
Index content for semantic search and clustering (non-blocking).
|
Index content for semantic search and clustering.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
items: List of (id, text, metadata) tuples.
|
items: List of (id, text, metadata) tuples.
|
||||||
"""
|
"""
|
||||||
<<<<<<< HEAD
|
await self._ensure_initialized_async()
|
||||||
# Check if already initialized
|
|
||||||
if not self._initialized and not self._initialization_in_progress:
|
|
||||||
# Trigger initialization in background (non-blocking)
|
|
||||||
self._ensure_initialized()
|
|
||||||
# Don't wait for initialization - let it happen in background
|
|
||||||
logger.debug(f"Indexing triggered for user {self.user_id}, initialization will happen in background")
|
|
||||||
return
|
|
||||||
|
|
||||||
# If initialization is still in progress, log and return without blocking
|
|
||||||
if not self._initialized:
|
|
||||||
logger.warning(f"Service not yet initialized for user {self.user_id}, indexing will retry later")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.embeddings:
|
|
||||||
logger.error(f"Cannot index content - embeddings not available for user {self.user_id}")
|
|
||||||
=======
|
|
||||||
self._ensure_initialized()
|
|
||||||
if not self._initialized or not self.embeddings:
|
if not self._initialized or not self.embeddings:
|
||||||
message = f"Cannot index content - service not initialized for user {self.user_id}"
|
message = f"Cannot index content - service not initialized for user {self.user_id}"
|
||||||
logger.error(message)
|
logger.warning(message)
|
||||||
if self.fail_fast:
|
if self.fail_fast:
|
||||||
raise RuntimeError(message)
|
raise RuntimeError(message)
|
||||||
return
|
return
|
||||||
@@ -294,7 +277,7 @@ class TxtaiIntelligenceService:
|
|||||||
|
|
||||||
async def search(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
async def search(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
||||||
"""Perform semantic search with intelligent caching."""
|
"""Perform semantic search with intelligent caching."""
|
||||||
self._ensure_initialized()
|
await self._ensure_initialized_async()
|
||||||
if not self._initialized or not self.embeddings:
|
if not self._initialized or not self.embeddings:
|
||||||
message = f"Cannot perform search - service not initialized for user {self.user_id}"
|
message = f"Cannot perform search - service not initialized for user {self.user_id}"
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
@@ -341,7 +324,7 @@ class TxtaiIntelligenceService:
|
|||||||
|
|
||||||
async def get_similarity(self, text1: str, text2: str) -> float:
|
async def get_similarity(self, text1: str, text2: str) -> float:
|
||||||
"""Get semantic similarity between two texts with caching."""
|
"""Get semantic similarity between two texts with caching."""
|
||||||
self._ensure_initialized()
|
await self._ensure_initialized_async()
|
||||||
if not self._initialized or not self.embeddings:
|
if not self._initialized or not self.embeddings:
|
||||||
logger.error(f"Cannot calculate similarity - service not initialized for user {self.user_id}")
|
logger.error(f"Cannot calculate similarity - service not initialized for user {self.user_id}")
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -410,7 +393,7 @@ class TxtaiIntelligenceService:
|
|||||||
|
|
||||||
async def cluster(self, min_score: float = 0.5) -> List[List[int]]:
|
async def cluster(self, min_score: float = 0.5) -> List[List[int]]:
|
||||||
"""Cluster indexed content to find semantic pillars using graph-based clustering with caching."""
|
"""Cluster indexed content to find semantic pillars using graph-based clustering with caching."""
|
||||||
self._ensure_initialized()
|
await self._ensure_initialized_async()
|
||||||
if not self._initialized or not self.embeddings:
|
if not self._initialized or not self.embeddings:
|
||||||
logger.error(f"Cannot cluster content - service not initialized for user {self.user_id}")
|
logger.error(f"Cannot cluster content - service not initialized for user {self.user_id}")
|
||||||
return []
|
return []
|
||||||
@@ -536,7 +519,7 @@ class TxtaiIntelligenceService:
|
|||||||
|
|
||||||
async def classify(self, text: str, labels: List[str]) -> List[Tuple[str, float]]:
|
async def classify(self, text: str, labels: List[str]) -> List[Tuple[str, float]]:
|
||||||
"""Classify text using zero-shot classification."""
|
"""Classify text using zero-shot classification."""
|
||||||
self._ensure_initialized()
|
await self._ensure_initialized_async()
|
||||||
if not self._initialized or not Labels:
|
if not self._initialized or not Labels:
|
||||||
logger.error(f"Cannot classify text - service not initialized or Labels not available for user {self.user_id}")
|
logger.error(f"Cannot classify text - service not initialized or Labels not available for user {self.user_id}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -97,13 +97,29 @@ HF_FALLBACK_MODELS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_model_variants(model: str):
|
||||||
|
"""Yield model ids to try for a single logical model preference."""
|
||||||
|
if not model:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try configured model first (supports provider suffixes like ":groq")
|
||||||
|
yield model
|
||||||
|
|
||||||
|
# Fallback to base repo id when provider suffix is not recognized by the router
|
||||||
|
if ":" in model:
|
||||||
|
base_model = model.split(":", 1)[0]
|
||||||
|
if base_model:
|
||||||
|
yield base_model
|
||||||
|
|
||||||
|
|
||||||
def _fallback_model_sequence(model: str):
|
def _fallback_model_sequence(model: str):
|
||||||
sequence = [model] + HF_FALLBACK_MODELS
|
sequence = [model] + HF_FALLBACK_MODELS
|
||||||
seen = set()
|
seen = set()
|
||||||
for candidate in sequence:
|
for preferred_model in sequence:
|
||||||
if candidate and candidate not in seen:
|
for candidate in _candidate_model_variants(preferred_model):
|
||||||
seen.add(candidate)
|
if candidate and candidate not in seen:
|
||||||
yield candidate
|
seen.add(candidate)
|
||||||
|
yield candidate
|
||||||
|
|
||||||
def get_huggingface_api_key() -> str:
|
def get_huggingface_api_key() -> str:
|
||||||
"""Get Hugging Face API key with proper error handling."""
|
"""Get Hugging Face API key with proper error handling."""
|
||||||
@@ -201,7 +217,7 @@ def huggingface_text_response(
|
|||||||
|
|
||||||
# Add debugging for API call
|
# Add debugging for API call
|
||||||
logger.info(
|
logger.info(
|
||||||
"Hugging Face text call | model=%s | prompt_len=%s | temp=%s | top_p=%s | max_tokens=%s",
|
"Hugging Face text call | model={} | prompt_len={} | temp={} | top_p={} | max_tokens={}",
|
||||||
model,
|
model,
|
||||||
len(prompt) if isinstance(prompt, str) else '<non-str>',
|
len(prompt) if isinstance(prompt, str) else '<non-str>',
|
||||||
temperature,
|
temperature,
|
||||||
@@ -227,11 +243,11 @@ def huggingface_text_response(
|
|||||||
max_tokens=max_tokens
|
max_tokens=max_tokens
|
||||||
)
|
)
|
||||||
if candidate_model != model:
|
if candidate_model != model:
|
||||||
logger.warning("HF text generation switched to fallback model: %s", candidate_model)
|
logger.warning("HF text generation switched to fallback model: {}", candidate_model)
|
||||||
break
|
break
|
||||||
except NotFoundError as nf_err:
|
except NotFoundError as nf_err:
|
||||||
last_error = nf_err
|
last_error = nf_err
|
||||||
logger.warning("HF model not found: %s. Trying fallback model.", candidate_model)
|
logger.warning("HF model not found: {}. Trying fallback model.", candidate_model)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
@@ -347,7 +363,7 @@ def huggingface_structured_json_response(
|
|||||||
|
|
||||||
# Add debugging for API call
|
# Add debugging for API call
|
||||||
logger.info(
|
logger.info(
|
||||||
"Hugging Face structured call | model=%s | prompt_len=%s | schema_kind=%s | temp=%s | max_tokens=%s",
|
"Hugging Face structured call | model={} | prompt_len={} | schema_kind={} | temp={} | max_tokens={}",
|
||||||
model,
|
model,
|
||||||
len(prompt) if isinstance(prompt, str) else '<non-str>',
|
len(prompt) if isinstance(prompt, str) else '<non-str>',
|
||||||
type(schema).__name__,
|
type(schema).__name__,
|
||||||
@@ -381,11 +397,11 @@ def huggingface_structured_json_response(
|
|||||||
response_format={"type": "json_object"} # Try to enforce JSON mode if supported
|
response_format={"type": "json_object"} # Try to enforce JSON mode if supported
|
||||||
)
|
)
|
||||||
if candidate_model != model:
|
if candidate_model != model:
|
||||||
logger.warning("HF structured generation switched to fallback model: %s", candidate_model)
|
logger.warning("HF structured generation switched to fallback model: {}", candidate_model)
|
||||||
break
|
break
|
||||||
except NotFoundError as nf_err:
|
except NotFoundError as nf_err:
|
||||||
last_error = nf_err
|
last_error = nf_err
|
||||||
logger.warning("HF structured model not found: %s. Trying fallback model.", candidate_model)
|
logger.warning("HF structured model not found: {}. Trying fallback model.", candidate_model)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
@@ -437,11 +453,11 @@ def huggingface_structured_json_response(
|
|||||||
max_tokens=max_tokens
|
max_tokens=max_tokens
|
||||||
)
|
)
|
||||||
if candidate_model != model:
|
if candidate_model != model:
|
||||||
logger.warning("HF structured no-response_format fallback model: %s", candidate_model)
|
logger.warning("HF structured no-response_format fallback model: {}", candidate_model)
|
||||||
break
|
break
|
||||||
except NotFoundError as nf_err:
|
except NotFoundError as nf_err:
|
||||||
last_error = nf_err
|
last_error = nf_err
|
||||||
logger.warning("HF structured model not found (no response_format path): %s", candidate_model)
|
logger.warning("HF structured model not found (no response_format path): {}", candidate_model)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
|
|||||||
@@ -231,6 +231,22 @@ export class ContentPlanningOrchestrator {
|
|||||||
|
|
||||||
// New approach: stream strategic intelligence data and show status from AI generation SSE
|
// New approach: stream strategic intelligence data and show status from AI generation SSE
|
||||||
return await new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>(async (resolve) => {
|
return await new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>(async (resolve) => {
|
||||||
|
let finished = false;
|
||||||
|
const complete = (payload: { aiInsights: any[]; aiRecommendations: any[] }) => {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
resolve(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hard timeout so the orchestrator never hangs if SSE never emits terminal events.
|
||||||
|
const hardTimeout = window.setTimeout(() => {
|
||||||
|
this.updateServiceStatus('aiAnalytics', {
|
||||||
|
status: 'error',
|
||||||
|
progress: 0,
|
||||||
|
message: 'Strategic intelligence timed out'
|
||||||
|
});
|
||||||
|
complete({ aiInsights: [], aiRecommendations: [] });
|
||||||
|
}, 45000);
|
||||||
// 1) Execution status stream (best-effort; ignore if no active strategy)
|
// 1) Execution status stream (best-effort; ignore if no active strategy)
|
||||||
try {
|
try {
|
||||||
const currentStrategyId = this.latestDashboardData?.strategies?.[0]?.id;
|
const currentStrategyId = this.latestDashboardData?.strategies?.[0]?.id;
|
||||||
@@ -280,18 +296,21 @@ export class ContentPlanningOrchestrator {
|
|||||||
});
|
});
|
||||||
// Map to orchestrator fields if needed
|
// Map to orchestrator fields if needed
|
||||||
this.notifyDataUpdate({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
this.notifyDataUpdate({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
||||||
resolve({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
window.clearTimeout(hardTimeout);
|
||||||
|
complete({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
|
||||||
} else if (data.type === 'error') {
|
} else if (data.type === 'error') {
|
||||||
this.updateServiceStatus('aiAnalytics', {
|
this.updateServiceStatus('aiAnalytics', {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
message: data.message || 'Failed to load strategic intelligence'
|
message: data.message || 'Failed to load strategic intelligence'
|
||||||
});
|
});
|
||||||
resolve({ aiInsights: [], aiRecommendations: [] });
|
window.clearTimeout(hardTimeout);
|
||||||
|
complete({ aiInsights: [], aiRecommendations: [] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
resolve({ aiInsights: [], aiRecommendations: [] });
|
window.clearTimeout(hardTimeout);
|
||||||
|
complete({ aiInsights: [], aiRecommendations: [] });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user