|
|
| from datetime import datetime |
| import json |
| import logging |
| import re |
| from typing import Any, Dict, Optional, Tuple |
|
|
| logger = logging.getLogger(__name__) |
|
|
| def parse_react_response(llm_output: str) -> Tuple[Optional[str], Optional[Dict[str, Any]], Optional[str]]: |
| """ |
| Parse the LLM output to extract Thought, Action, and Final Answer. |
| |
| Expected Formats: |
| |
| Format 1 (JSON Action): |
| Thought: I need to search for X. |
| Action: { |
| "tool": "tool_name", |
| "params": { ... } |
| } |
| |
| Format 2 (Final Answer): |
| Thought: I have found the info. |
| Final Answer: The answer is X. |
| |
| Returns: |
| (thought_text, action_dict, final_answer_text) |
| """ |
| |
| text = llm_output.strip() |
| |
| thought = None |
| action = None |
| final_answer = None |
| |
| |
| |
| |
| thought_match = re.search(r"Thought:\s*(.*?)(?=\n\s*Action:|\n\s*Final Answer:|$)", text, re.DOTALL | re.IGNORECASE) |
| if thought_match: |
| thought = thought_match.group(1).strip() |
| else: |
| |
| |
| if not text.lower().strip().startswith("action:") and not text.lower().strip().startswith("final answer:"): |
| |
| split_match = re.split(r"\n\s*Action:|\n\s*Final Answer:", text, flags=re.IGNORECASE) |
| if split_match: |
| thought = split_match[0].strip() |
|
|
| |
| final_answer_match = re.search(r"Final Answer:\s*(.*)", text, re.DOTALL | re.IGNORECASE) |
| if final_answer_match: |
| final_answer = final_answer_match.group(1).strip() |
| return thought, None, final_answer |
|
|
| |
| |
| action_match = re.search(r"Action:\s*(.*)", text, re.DOTALL | re.IGNORECASE) |
| if action_match: |
| action_text = action_match.group(1).strip() |
|
|
| |
| |
| try: |
| json_start = action_text.find("{") |
| json_end = action_text.rfind("}") |
|
|
| if json_start != -1 and json_end != -1: |
| json_str = action_text[json_start:json_end+1] |
| action = json.loads(json_str) |
| else: |
| |
| logger.warning(f"Action found but no valid JSON structure: {action_text[:200]}") |
| raise ValueError(f"Invalid action format: no JSON structure found in '{action_text[:100]}...'") |
|
|
| except json.JSONDecodeError as e: |
| |
| logger.error(f"Failed to parse agent action JSON: {e}\nAction text: {action_text[:500]}") |
| raise ValueError(f"Invalid JSON in agent action: {e}") from e |
|
|
| return thought, action, final_answer |
|
|
|
|
| |
| |
| |
|
|
| def format_agent_id(agent_id: str) -> str: |
| """ |
| Format agent ID for display. |
| |
| Args: |
| agent_id: Raw agent ID (e.g., "agent-abc123") |
| |
| Returns: |
| Formatted agent ID with consistent capitalization and spacing |
| |
| Examples: |
| >>> format_agent_id("agent-abc123") |
| 'Agent ABC123' |
| >>> format_agent_id("workflow-xyz") |
| 'Workflow XYZ' |
| """ |
| if not agent_id: |
| return "Unknown Agent" |
|
|
| |
| parts = re.split(r'[-_]', agent_id) |
|
|
| |
| formatted_parts = [part.upper() if part.lower() in ['id', 'uuid'] else part.capitalize() for part in parts if part] |
|
|
| return ' '.join(formatted_parts) |
|
|
|
|
| def parse_agent_id(agent_id: str) -> Dict[str, Optional[str]]: |
| """ |
| Parse agent ID into components. |
| |
| Args: |
| agent_id: Agent ID string (e.g., "agent-abc123" or "workflow-v2-test") |
| |
| Returns: |
| Dict with 'type', 'id', 'version' keys |
| |
| Examples: |
| >>> parse_agent_id("agent-abc123") |
| {'type': 'agent', 'id': 'abc123', 'version': None} |
| >>> parse_agent_id("workflow-v2-test") |
| {'type': 'workflow', 'id': 'test', 'version': 'v2'} |
| """ |
| if not agent_id: |
| return {'type': None, 'id': None, 'version': None} |
|
|
| parts = agent_id.split('-') |
|
|
| result = { |
| 'type': parts[0] if len(parts) > 0 else None, |
| 'id': parts[-1] if len(parts) > 1 else None, |
| 'version': None |
| } |
|
|
| |
| for part in parts: |
| if re.match(r'^v\d+$', part.lower()): |
| result['version'] = part.lower() |
| |
| if result['id'] == part: |
| result['id'] = parts[-2] if len(parts) > 2 else None |
|
|
| return result |
|
|
|
|
| def format_maturity_level(maturity: str) -> str: |
| """ |
| Format maturity level for display. |
| |
| Args: |
| maturity: Maturity level (STUDENT, INTERN, SUPERVISED, AUTONOMOUS) |
| |
| Returns: |
| Formatted display name |
| |
| Examples: |
| >>> format_maturity_level("STUDENT") |
| 'Student Agent' |
| >>> format_maturity_level("AUTONOMOUS") |
| 'Autonomous Agent' |
| """ |
| maturity_map = { |
| 'STUDENT': 'Student Agent', |
| 'INTERN': 'Intern Agent', |
| 'SUPERVISED': 'Supervised Agent', |
| 'AUTONOMOUS': 'Autonomous Agent' |
| } |
|
|
| return maturity_map.get(maturity.upper(), maturity.capitalize() + ' Agent') |
|
|
|
|
| |
| |
| |
|
|
| def format_date_iso(date_string: str) -> str: |
| """ |
| Format ISO date string to readable format. |
| |
| Args: |
| date_string: ISO date string (YYYY-MM-DD) |
| |
| Returns: |
| Formatted date (e.g., "March 8, 2026") |
| |
| Examples: |
| >>> format_date_iso("2026-03-08") |
| 'March 8, 2026' |
| """ |
| try: |
| date_obj = datetime.fromisoformat(date_string) |
| return date_obj.strftime("%B %d, %Y").replace(" 0", " ") |
| except (ValueError, AttributeError, TypeError): |
| return date_string |
|
|
|
|
| def format_datetime(date_string: str) -> str: |
| """ |
| Format ISO datetime string to readable format with time. |
| |
| Args: |
| date_string: ISO datetime string (YYYY-MM-DDTHH:MM:SS) |
| |
| Returns: |
| Formatted datetime (e.g., "March 8, 2026 at 2:30 PM") |
| |
| Examples: |
| >>> format_datetime("2026-03-08T14:30:00") |
| 'March 8, 2026 at 2:30 PM' |
| """ |
| try: |
| date_obj = datetime.fromisoformat(date_string.replace('Z', '+00:00')) |
| return date_obj.strftime("%B %d, %Y at %I:%M %p").replace(" 0", " ").lstrip('0') |
| except (ValueError, AttributeError): |
| return date_string |
|
|
|
|
| def format_timestamp(timestamp: float) -> str: |
| """ |
| Format Unix timestamp to readable date. |
| |
| Args: |
| timestamp: Unix timestamp (seconds since epoch) |
| |
| Returns: |
| Formatted date (e.g., "March 8, 2026") |
| |
| Examples: |
| >>> format_timestamp(1772974705) |
| 'March 8, 2026' |
| """ |
| try: |
| date_obj = datetime.utcfromtimestamp(timestamp) |
| return date_obj.strftime("%B %d, %Y").replace(" 0", " ") |
| except (ValueError, TypeError, OSError): |
| return str(timestamp) |
|
|
|
|
| def format_relative_time(timestamp: float) -> str: |
| """ |
| Format timestamp as relative time (e.g., "2 hours ago"). |
| |
| Args: |
| timestamp: Unix timestamp |
| |
| Returns: |
| Relative time string |
| |
| Examples: |
| >>> format_relative_time(time.time() - 3600) |
| '1 hour ago' |
| """ |
| try: |
| from datetime import timezone |
| now = datetime.now(timezone.utc) |
| past = datetime.fromtimestamp(timestamp, tz=timezone.utc) |
| diff = now - past |
|
|
| seconds = diff.total_seconds() |
|
|
| if seconds < 60: |
| return "just now" |
| elif seconds < 3600: |
| minutes = int(seconds / 60) |
| return f"{minutes} minute{'s' if minutes != 1 else ''} ago" |
| elif seconds < 86400: |
| hours = int(seconds / 3600) |
| return f"{hours} hour{'s' if hours != 1 else ''} ago" |
| elif seconds < 604800: |
| days = int(seconds / 86400) |
| return f"{days} day{'s' if days != 1 else ''} ago" |
| else: |
| weeks = int(seconds / 604800) |
| return f"{weeks} week{'s' if weeks != 1 else ''} ago" |
| except (ValueError, TypeError, OSError): |
| return "unknown time" |
|
|
|
|
| |
| |
| |
|
|
| def format_currency_usd(amount: float) -> str: |
| """ |
| Format amount as USD currency. |
| |
| Args: |
| amount: Amount in USD |
| |
| Returns: |
| Formatted currency string (e.g., "$1,000.00") |
| |
| Examples: |
| >>> format_currency_usd(1000) |
| '$1,000.00' |
| """ |
| try: |
| if amount < 0: |
| return f"-${abs(amount):,.2f}" |
| return f"${amount:,.2f}" |
| except (ValueError, TypeError): |
| return "$0.00" |
|
|
|
|
| def format_currency_eur(amount: float) -> str: |
| """ |
| Format amount as EUR currency. |
| |
| Args: |
| amount: Amount in EUR |
| |
| Returns: |
| Formatted currency string (e.g., "€1,000.00") |
| |
| Examples: |
| >>> format_currency_eur(1000) |
| '€1,000.00' |
| """ |
| try: |
| return f"€{amount:,.2f}" |
| except (ValueError, TypeError): |
| return "€0.00" |
|
|
|
|
| def format_number(number: int) -> str: |
| """ |
| Format number with thousands separators. |
| |
| Args: |
| number: Number to format |
| |
| Returns: |
| Formatted number string (e.g., "1,000,000") |
| |
| Examples: |
| >>> format_number(1000000) |
| '1,000,000' |
| """ |
| try: |
| return f"{number:,}" |
| except (ValueError, TypeError): |
| return "0" |
|
|
|
|
| def format_percentage(value: float, decimals: int = 1) -> str: |
| """ |
| Format value as percentage. |
| |
| Args: |
| value: Value as decimal (0.5 = 50%) |
| decimals: Number of decimal places |
| |
| Returns: |
| Formatted percentage string (e.g., "50.5%") |
| |
| Examples: |
| >>> format_percentage(0.505) |
| '50.5%' |
| """ |
| try: |
| return f"{value * 100:.{decimals}f}%" |
| except (ValueError, TypeError): |
| return "0.0%" |
|
|
|
|
| def format_decimal(value: float, precision: int = 2) -> str: |
| """ |
| Format value with specified decimal precision. |
| |
| Args: |
| value: Value to format |
| precision: Number of decimal places |
| |
| Returns: |
| Formatted decimal string |
| |
| Examples: |
| >>> format_decimal(3.14159, 2) |
| '3.14' |
| """ |
| try: |
| return f"{value:.{precision}f}" |
| except (ValueError, TypeError): |
| return "0.00" |
|
|
|
|
| |
| |
| |
|
|
| def format_phone(phone: str) -> str: |
| """ |
| Format phone number to US format. |
| |
| Args: |
| phone: Phone number string (digits only or with formatting) |
| |
| Returns: |
| Formatted phone number (e.g., "(123) 456-7890") |
| |
| Examples: |
| >>> format_phone("1234567890") |
| '(123) 456-7890' |
| """ |
| if not phone: |
| return "" |
|
|
| |
| digits = re.sub(r'\D', '', phone) |
|
|
| |
| if len(digits) == 10: |
| return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}" |
| elif len(digits) == 11 and digits[0] == '1': |
| return f"+1 ({digits[1:4]}) {digits[4:7]}-{digits[7:]}" |
| else: |
| |
| return phone |
|
|
|
|
| def format_name(name: str) -> str: |
| """ |
| Format name with proper capitalization. |
| |
| Args: |
| name: Name string |
| |
| Returns: |
| Formatted name with each word capitalized |
| |
| Examples: |
| >>> format_name("john doe") |
| 'John Doe' |
| """ |
| if not name: |
| return "" |
|
|
| |
| return ' '.join(word.capitalize() for word in name.split()) |
|
|
|
|
| def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str: |
| """ |
| Truncate text to maximum length with suffix. |
| |
| Args: |
| text: Text to truncate |
| max_length: Maximum length (including suffix) |
| suffix: Suffix to add when truncated |
| |
| Returns: |
| Truncated text |
| |
| Examples: |
| >>> truncate_text("This is a very long text", 10) |
| 'This is...' |
| """ |
| if not text: |
| return "" |
|
|
| if len(text) <= max_length: |
| return text |
|
|
| |
| if max_length <= len(suffix): |
| return suffix[:max_length] |
|
|
| |
| return text[:max_length - len(suffix)] + suffix |
|
|
|
|
| def sanitize_string(text: str, remove_html: bool = True, remove_special: bool = False) -> str: |
| """ |
| Sanitize string by removing HTML and/or special characters. |
| |
| Args: |
| text: Text to sanitize |
| remove_html: Remove HTML tags |
| remove_special: Remove special characters (keep alphanumeric and spaces) |
| |
| Returns: |
| Sanitized string |
| |
| Examples: |
| >>> sanitize_string("<p>Hello</p>") |
| 'Hello' |
| """ |
| if not text: |
| return "" |
|
|
| result = text |
|
|
| |
| if remove_html: |
| result = re.sub(r'<[^>]+>', '', result) |
|
|
| |
| if remove_special: |
| result = re.sub(r'[^a-zA-Z0-9\s]', '', result) |
|
|
| return result.strip() |
|
|