Download server.py from santanche/sml-agents-publish-subscribe: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/spaces/santanche/sml-agents-publish-subscribe/resolve/main/server.py
- Command line
-
hf download hf://spaces/santanche/sml-agents-publish-subscribe/server.py
-
curl -L -o server.py https://huggingface.co/spaces/santanche/sml-agents-publish-subscribe/resolve/main/server.py
12.3 kB
| from fastapi import FastAPI | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import StreamingResponse, FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel | |
| from langchain_community.llms import Ollama | |
| from langchain.prompts import PromptTemplate | |
| from typing import List, Dict, Optional, AsyncGenerator | |
| import json | |
| import asyncio | |
| from pathlib import Path | |
| import os | |
| import re | |
| # Import transformers for NER | |
| try: | |
| from transformers import pipeline | |
| TRANSFORMERS_AVAILABLE = True | |
| except ImportError: | |
| TRANSFORMERS_AVAILABLE = False | |
| print("Warning: transformers not available, NER models will not work") | |
| app = FastAPI(title="Pub/Sub Multi-Agent System") | |
| # Enable CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Mount static files | |
| static_dir = Path(__file__).parent / "static" | |
| static_dir.mkdir(exist_ok=True) | |
| app.mount("/static", StaticFiles(directory=static_dir), name="static") | |
| # Models | |
| class DataSource(BaseModel): | |
| label: str | |
| content: str | |
| class Agent(BaseModel): | |
| title: str | |
| prompt: str | |
| model: str | |
| subscribe_topic: str | |
| publish_topic: Optional[str] = None | |
| show_result: bool = False | |
| class ExecutionRequest(BaseModel): | |
| data_sources: List[DataSource] | |
| user_question: str = "" | |
| agents: List[Agent] | |
| # Pub/Sub Bus | |
| class MessageBus: | |
| def __init__(self): | |
| self.subscribers: Dict[str, List[Agent]] = {} | |
| self.messages: Dict[str, str] = {} | |
| def reset(self): | |
| """Reset the bus for a new execution""" | |
| self.subscribers = {} | |
| self.messages = {} | |
| def _normalize_topic(self, topic: str) -> str: | |
| """Normalize topic to lowercase for case-insensitive matching""" | |
| return topic.lower().strip() | |
| def subscribe(self, topic: str, agent: Agent): | |
| """Subscribe an agent to a topic (case insensitive)""" | |
| normalized = self._normalize_topic(topic) | |
| if normalized not in self.subscribers: | |
| self.subscribers[normalized] = [] | |
| self.subscribers[normalized].append(agent) | |
| def publish(self, topic: str, content: str): | |
| """Publish a message to a topic (case insensitive)""" | |
| normalized = self._normalize_topic(topic) | |
| self.messages[normalized] = content | |
| def get_message(self, topic: str) -> Optional[str]: | |
| """Get message from a topic (case insensitive)""" | |
| normalized = self._normalize_topic(topic) | |
| return self.messages.get(normalized) | |
| def get_subscribers(self, topic: str) -> List[Agent]: | |
| """Get all subscribers for a topic (case insensitive)""" | |
| normalized = self._normalize_topic(topic) | |
| return self.subscribers.get(normalized, []) | |
| # Stream event helper | |
| def create_event(event_type: str, **kwargs): | |
| data = {"type": event_type, **kwargs} | |
| return f"data: {json.dumps(data)}\n\n" | |
| # Get LLM instance | |
| def get_llm(model_name: str): | |
| return Ollama(model=model_name, temperature=0.1) | |
| # NER pipeline cache | |
| _ner_pipelines = {} | |
| def get_ner_pipeline(model_name: str): | |
| """Get or create NER pipeline for the specified model""" | |
| if not TRANSFORMERS_AVAILABLE: | |
| raise RuntimeError("transformers package not available") | |
| if model_name not in _ner_pipelines: | |
| print(f"Loading NER model: {model_name}") | |
| _ner_pipelines[model_name] = pipeline( | |
| "ner", | |
| model=model_name, | |
| aggregation_strategy="simple" | |
| ) | |
| return _ner_pipelines[model_name] | |
| # Check if model is NER model | |
| def is_ner_model(model_name: str) -> bool: | |
| """Check if the model is an NER model""" | |
| ner_models = [ | |
| "samrawal/bert-base-uncased_clinical-ner", | |
| "OpenMed/OpenMed-NER-AnatomyDetect-BioPatient-108M" | |
| ] | |
| return model_name in ner_models | |
| # Format NER output for display | |
| def format_ner_result(text: str, entities: List[Dict]) -> str: | |
| """Format NER entities for human-readable display""" | |
| if not entities: | |
| return text | |
| # Sort entities by start position in reverse to avoid index issues | |
| sorted_entities = sorted(entities, key=lambda x: x['start'], reverse=True) | |
| result = text | |
| for entity in sorted_entities: | |
| start = entity['start'] | |
| end = entity['end'] | |
| entity_type = entity['entity_type'] | |
| original_text = text[start:end] | |
| # Replace entity with labeled version | |
| labeled = f"[{original_text}:{entity_type}]" | |
| result = result[:start] + labeled + result[end:] | |
| return result | |
| # Process NER with transformers pipeline | |
| def process_ner(text: str, model_name: str) -> tuple[str, List[Dict]]: | |
| """Process text with NER pipeline and return JSON + formatted entities""" | |
| try: | |
| ner_pipeline = get_ner_pipeline(model_name) | |
| # Run NER | |
| entities = ner_pipeline(text) | |
| # Convert to our format with proper type conversion | |
| formatted_entities = [] | |
| for entity in entities: | |
| formatted_entities.append({ | |
| "text": str(entity['word']), | |
| "entity_type": str(entity['entity_group']), | |
| "start": int(entity['start']), | |
| "end": int(entity['end']), | |
| "score": float(entity.get('score', 0.0)) # Convert numpy float32 to Python float | |
| }) | |
| # Create JSON output with proper serialization | |
| json_output = json.dumps(formatted_entities, indent=2) | |
| return json_output, formatted_entities | |
| except Exception as e: | |
| error_msg = f"NER processing failed: {str(e)}" | |
| return json.dumps({"error": error_msg}), [] | |
| # Execute agent | |
| async def execute_agent(agent: Agent, input_content: str, data_sources: List[DataSource], user_question: str) -> tuple[str, Optional[List[Dict]], Optional[str]]: | |
| """Execute a single agent with the given input. Returns (result, entities, analyzed_text) where entities is for NER models.""" | |
| # Case-insensitive replacement helper | |
| def replace_case_insensitive(text: str, placeholder: str, value: str) -> str: | |
| """Replace placeholder in text, case insensitive""" | |
| pattern = re.compile(re.escape(placeholder), re.IGNORECASE) | |
| return pattern.sub(value, text) | |
| # Start with the agent's prompt template | |
| prompt_text = agent.prompt if agent.prompt else "" | |
| # Replace standard placeholders (case insensitive) | |
| prompt_text = replace_case_insensitive(prompt_text, "{input}", input_content) | |
| prompt_text = replace_case_insensitive(prompt_text, "{question}", user_question) | |
| # Replace data source placeholders (case insensitive) | |
| for ds in data_sources: | |
| placeholder = "{" + ds.label + "}" | |
| prompt_text = replace_case_insensitive(prompt_text, placeholder, ds.content) | |
| # Check if this is an NER model | |
| if is_ner_model(agent.model): | |
| # For NER models, the rendered prompt IS the text to analyze | |
| text_to_analyze = prompt_text | |
| # If prompt is empty, use input content directly | |
| if not text_to_analyze.strip(): | |
| text_to_analyze = input_content | |
| # Process with NER pipeline | |
| json_result, entities = process_ner(text_to_analyze, agent.model) | |
| # Return JSON result, entities, and the text that was analyzed | |
| return json_result, entities, text_to_analyze | |
| else: | |
| # Regular LLM processing | |
| llm = get_llm(agent.model) | |
| # Invoke LLM with the rendered prompt | |
| result = llm.invoke(prompt_text) | |
| return (result if isinstance(result, str) else str(result)), None, None | |
| # Main execution pipeline | |
| async def execute_pipeline(request: ExecutionRequest) -> AsyncGenerator[str, None]: | |
| try: | |
| bus = MessageBus() | |
| yield create_event("bus_init") | |
| # Reset and configure subscriptions | |
| bus.reset() | |
| # Subscribe all agents to their topics | |
| for agent in request.agents: | |
| if agent.subscribe_topic: | |
| bus.subscribe(agent.subscribe_topic, agent) | |
| yield create_event("agent_subscribed", agent=agent.title, topic=agent.subscribe_topic) | |
| # Publish START message | |
| start_message = request.user_question if request.user_question else "System initialized" | |
| bus.publish("START", start_message) | |
| yield create_event("message_published", topic="START", content=start_message) | |
| # Process messages in the bus | |
| processed_topics = set() | |
| max_iterations = 20 # Prevent infinite loops | |
| iteration = 0 | |
| while iteration < max_iterations: | |
| iteration += 1 | |
| # Find topics that have messages but haven't been processed | |
| topics_to_process = [topic for topic in bus.messages.keys() if topic not in processed_topics] | |
| if not topics_to_process: | |
| break | |
| for topic in topics_to_process: | |
| subscribers = bus.get_subscribers(topic) | |
| if not subscribers: | |
| yield create_event("no_subscribers", topic=topic) | |
| processed_topics.add(topic) | |
| continue | |
| message_content = bus.get_message(topic) | |
| for agent in subscribers: | |
| yield create_event("agent_triggered", agent=agent.title, topic=topic) | |
| yield create_event("agent_processing", agent=agent.title) | |
| yield create_event("agent_input", content=message_content) | |
| # Execute agent | |
| try: | |
| result, entities, analyzed_text = await execute_agent(agent, message_content, request.data_sources, request.user_question) | |
| yield create_event("agent_output", content=result) | |
| # If agent wants to show result, send it to frontend | |
| if agent.show_result: | |
| yield create_event("show_result", agent=agent.title, content=result) | |
| # If this is an NER agent with entities, also send formatted NER result | |
| if entities and is_ner_model(agent.model) and analyzed_text: | |
| formatted_text = format_ner_result(analyzed_text, entities) | |
| yield create_event("ner_result", agent=agent.title, formatted_text=formatted_text) | |
| # Publish result to agent's publish topic (if specified) | |
| if agent.publish_topic: | |
| bus.publish(agent.publish_topic, result) | |
| yield create_event("message_published", topic=agent.publish_topic, content=result) | |
| yield create_event("agent_completed", agent=agent.title) | |
| except Exception as e: | |
| yield create_event("error", message=f"Agent {agent.title} failed: {str(e)}") | |
| processed_topics.add(topic) | |
| yield create_event("execution_complete") | |
| except Exception as e: | |
| yield create_event("error", message=str(e)) | |
| async def root(): | |
| """Serve the main web interface""" | |
| index_file = static_dir / "index.html" | |
| if index_file.exists(): | |
| return FileResponse(index_file) | |
| return {"message": "Place index.html in the static/ directory"} | |
| async def execute(request: ExecutionRequest): | |
| """Execute the pub/sub agent system with streaming logs""" | |
| return StreamingResponse( | |
| execute_pipeline(request), | |
| media_type="text/event-stream" | |
| ) | |
| async def health(): | |
| """Health check endpoint""" | |
| return {"status": "ok"} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| port = int(os.environ.get("PORT", 7860)) | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |