Spaces:
Sleeping
Sleeping
Update rich tool: bugifx,
Browse files- update prompts
- update providers
- agents/single_agent.py +4 -4
- api_utils.py +26 -0
- config.py +6 -5
- run_single_agent.py +9 -13
- tools/analysis_tools.py +18 -4
- tools/formatting_tools.py +186 -169
- tools/search_tools.py +75 -3
agents/single_agent.py
CHANGED
|
@@ -8,7 +8,7 @@ from smolagents import CodeAgent, VisitWebpageTool, FinalAnswerTool, DuckDuckGoS
|
|
| 8 |
from config import load_prompt_templates
|
| 9 |
from tools.analysis_tools import AnalyzeLyricsTool
|
| 10 |
from tools.formatting_tools import FormatAnalysisResultsTool
|
| 11 |
-
from tools.search_tools import ThrottledDuckDuckGoSearchTool
|
| 12 |
|
| 13 |
|
| 14 |
def create_single_agent(model):
|
|
@@ -27,12 +27,12 @@ def create_single_agent(model):
|
|
| 27 |
# Example usage within the agent
|
| 28 |
agent = CodeAgent(
|
| 29 |
tools=[
|
| 30 |
-
|
| 31 |
-
DuckDuckGoSearchTool(),
|
| 32 |
# ThrottledDuckDuckGoSearchTool(min_delay=7.0, max_delay=15.0),
|
| 33 |
VisitWebpageTool(),
|
| 34 |
AnalyzeLyricsTool(),
|
| 35 |
-
FormatAnalysisResultsTool()
|
|
|
|
| 36 |
],
|
| 37 |
model=model,
|
| 38 |
additional_authorized_imports=['numpy', 'bs4', 'rich'],
|
|
|
|
| 8 |
from config import load_prompt_templates
|
| 9 |
from tools.analysis_tools import AnalyzeLyricsTool
|
| 10 |
from tools.formatting_tools import FormatAnalysisResultsTool
|
| 11 |
+
from tools.search_tools import ThrottledDuckDuckGoSearchTool, BraveSearchTool
|
| 12 |
|
| 13 |
|
| 14 |
def create_single_agent(model):
|
|
|
|
| 27 |
# Example usage within the agent
|
| 28 |
agent = CodeAgent(
|
| 29 |
tools=[
|
| 30 |
+
BraveSearchTool(),
|
|
|
|
| 31 |
# ThrottledDuckDuckGoSearchTool(min_delay=7.0, max_delay=15.0),
|
| 32 |
VisitWebpageTool(),
|
| 33 |
AnalyzeLyricsTool(),
|
| 34 |
+
FormatAnalysisResultsTool(),
|
| 35 |
+
FinalAnswerTool(),
|
| 36 |
],
|
| 37 |
model=model,
|
| 38 |
additional_authorized_imports=['numpy', 'bs4', 'rich'],
|
api_utils.py
CHANGED
|
@@ -40,6 +40,32 @@ def make_api_call_with_retry(model: str, prompt: str) -> str:
|
|
| 40 |
model=model,
|
| 41 |
messages=[{"role": "user", "content": prompt}],
|
| 42 |
num_retries=2, # Built-in retry mechanism of LiteLLM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
)
|
| 44 |
|
| 45 |
# Try to extract the content from the response
|
|
|
|
| 40 |
model=model,
|
| 41 |
messages=[{"role": "user", "content": prompt}],
|
| 42 |
num_retries=2, # Built-in retry mechanism of LiteLLM
|
| 43 |
+
response_format={
|
| 44 |
+
"type": "json_object",
|
| 45 |
+
"schema": {
|
| 46 |
+
"type": "object",
|
| 47 |
+
"properties": {
|
| 48 |
+
"summary": {"type": "string", "description": "Overall analysis of the song vibes, meaning and mood"},
|
| 49 |
+
"main_themes": {"type": "array", "items": {"type": "string"}, "description": "Main themes identified in the song"},
|
| 50 |
+
"mood": {"type": "string", "description": "The overall mood/emotion of the song"},
|
| 51 |
+
"sections_analysis": {
|
| 52 |
+
"type": "array",
|
| 53 |
+
"items": {
|
| 54 |
+
"type": "object",
|
| 55 |
+
"properties": {
|
| 56 |
+
"section_type": {"type": "string", "description": "verse/chorus/bridge/etc."},
|
| 57 |
+
"section_number": {"type": "integer", "description": "Sequential number of this section type"},
|
| 58 |
+
"lines": {"type": "array", "items": {"type": "string"}, "description": "Lyrics of this section"},
|
| 59 |
+
"analysis": {"type": "string", "description": "Analysis of this section with respect to the overall theme"}
|
| 60 |
+
},
|
| 61 |
+
"required": ["section_type", "section_number", "lines", "analysis"]
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"conclusion": {"type": "string", "description": "The song vibes and concepts of the underlying meaning, including ideas author may have intended to express"}
|
| 65 |
+
},
|
| 66 |
+
"required": ["summary", "main_themes", "mood", "sections_analysis", "conclusion"]
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
)
|
| 70 |
|
| 71 |
# Try to extract the content from the response
|
config.py
CHANGED
|
@@ -26,7 +26,7 @@ def load_api_keys():
|
|
| 26 |
os.environ["GEMINI_API_KEY"] = os.getenv("GEMINI_API_KEY")
|
| 27 |
|
| 28 |
|
| 29 |
-
def get_model_id(
|
| 30 |
"""Get the appropriate model ID based on configuration.
|
| 31 |
|
| 32 |
Args:
|
|
@@ -38,14 +38,15 @@ def get_model_id(use_local=True, provider="gemini"):
|
|
| 38 |
String with model ID for the specified provider.
|
| 39 |
"""
|
| 40 |
if provider == "ollama":
|
| 41 |
-
return "ollama/gemma3:4b" # Using local Ollama with Gemma 3:4B
|
|
|
|
| 42 |
elif provider == "gemini":
|
| 43 |
return "gemini/gemini-2.0-flash"
|
| 44 |
elif provider == "openrouter":
|
| 45 |
return "openrouter/google/gemini-2.0-flash-lite-preview-02-05:free" # OpenRouter Claude 3 Opus
|
| 46 |
-
|
| 47 |
-
#
|
| 48 |
-
return "
|
| 49 |
|
| 50 |
def get_ollama_api_base():
|
| 51 |
"""Get the API base URL for Ollama."""
|
|
|
|
| 26 |
os.environ["GEMINI_API_KEY"] = os.getenv("GEMINI_API_KEY")
|
| 27 |
|
| 28 |
|
| 29 |
+
def get_model_id(provider="gemini"):
|
| 30 |
"""Get the appropriate model ID based on configuration.
|
| 31 |
|
| 32 |
Args:
|
|
|
|
| 38 |
String with model ID for the specified provider.
|
| 39 |
"""
|
| 40 |
if provider == "ollama":
|
| 41 |
+
# return "ollama/gemma3:4b" # Using local Ollama with Gemma 3:4B
|
| 42 |
+
return "ollama/qwen2.5-coder:7b"
|
| 43 |
elif provider == "gemini":
|
| 44 |
return "gemini/gemini-2.0-flash"
|
| 45 |
elif provider == "openrouter":
|
| 46 |
return "openrouter/google/gemini-2.0-flash-lite-preview-02-05:free" # OpenRouter Claude 3 Opus
|
| 47 |
+
# return "openrouter/mistralai/mistral-small-3.1-24b-instruct:free"
|
| 48 |
+
#return "openrouter/rekaai/reka-flash-3:free"
|
| 49 |
+
# return "openrouter/deepseek/deepseek-chat:free"
|
| 50 |
|
| 51 |
def get_ollama_api_base():
|
| 52 |
"""Get the API base URL for Ollama."""
|
run_single_agent.py
CHANGED
|
@@ -14,16 +14,12 @@ os.environ["GEMINI_API_KEY"] = str(os.getenv("GEMINI_API_KEY"))
|
|
| 14 |
|
| 15 |
use_local = False
|
| 16 |
|
| 17 |
-
|
| 18 |
-
# Initialize the LLM model based on configuration
|
| 19 |
-
if use_local:
|
| 20 |
-
model_id = "openrouter/google/gemini-2.0-flash-lite-preview-02-05:free"
|
| 21 |
-
else:
|
| 22 |
-
model_id = get_model_id(use_local=use_local)
|
| 23 |
-
|
| 24 |
logger.info(f"Initializing with model: {model_id}")
|
| 25 |
|
| 26 |
if use_local:
|
|
|
|
|
|
|
| 27 |
api_base = get_ollama_api_base()
|
| 28 |
logger.info(f"Using Ollama API base: {api_base}")
|
| 29 |
model = LiteLLMModel(model_id=model_id, api_base=api_base)
|
|
@@ -34,12 +30,12 @@ else:
|
|
| 34 |
# model_id='https://pflgm2locj2t89co.us-east-1.aws.endpoints.huggingface.cloud'
|
| 35 |
|
| 36 |
# Prompt the user for the song name
|
| 37 |
-
song_data = "
|
| 38 |
|
| 39 |
agent = create_single_agent(model)
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
# Agent execution
|
| 42 |
-
agent.run(
|
| 43 |
-
1. Find and extract the lyrics of the song, {song_data}. Don't try to scrape from azlyrics.com or genius.com, others are ok.
|
| 44 |
-
2. Perform deep lyrics analysis and return full lyrics and analysis results in a pretty human-readable format.
|
| 45 |
-
""")
|
|
|
|
| 14 |
|
| 15 |
use_local = False
|
| 16 |
|
| 17 |
+
model_id = get_model_id(provider='openrouter')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
logger.info(f"Initializing with model: {model_id}")
|
| 19 |
|
| 20 |
if use_local:
|
| 21 |
+
# If using Ollama, we need to specify the API base URL
|
| 22 |
+
# Initialize the LLM model based onx configuration
|
| 23 |
api_base = get_ollama_api_base()
|
| 24 |
logger.info(f"Using Ollama API base: {api_base}")
|
| 25 |
model = LiteLLMModel(model_id=model_id, api_base=api_base)
|
|
|
|
| 30 |
# model_id='https://pflgm2locj2t89co.us-east-1.aws.endpoints.huggingface.cloud'
|
| 31 |
|
| 32 |
# Prompt the user for the song name
|
| 33 |
+
song_data = "Felix Da Housecat - Everyone is someone in LA"
|
| 34 |
|
| 35 |
agent = create_single_agent(model)
|
| 36 |
+
prompt = f"""1. Find and extract the lyrics of the song: {song_data}.
|
| 37 |
+
2. Perform deep lyrics analysis and return full lyrics and analysis results
|
| 38 |
+
in a pretty human-readable format.
|
| 39 |
+
"""
|
| 40 |
# Agent execution
|
| 41 |
+
agent.run(prompt)
|
|
|
|
|
|
|
|
|
tools/analysis_tools.py
CHANGED
|
@@ -4,6 +4,8 @@ Analysis tools for understanding and interpreting song lyrics.
|
|
| 4 |
|
| 5 |
from loguru import logger
|
| 6 |
from smolagents import Tool
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from api_utils import make_api_call_with_retry
|
| 9 |
|
|
@@ -67,11 +69,23 @@ class AnalyzeLyricsTool(Tool):
|
|
| 67 |
{lyrics}
|
| 68 |
"""
|
| 69 |
|
| 70 |
-
model_to_use = "gemini/gemini-2.0-flash"
|
| 71 |
-
|
|
|
|
| 72 |
|
| 73 |
# Use the function with retry mechanism
|
| 74 |
logger.info("Analyzing lyrics for song: '{}' by '{}'", song_title, artist)
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
|
|
|
| 4 |
|
| 5 |
from loguru import logger
|
| 6 |
from smolagents import Tool
|
| 7 |
+
import json
|
| 8 |
+
from typing import Dict
|
| 9 |
|
| 10 |
from api_utils import make_api_call_with_retry
|
| 11 |
|
|
|
|
| 69 |
{lyrics}
|
| 70 |
"""
|
| 71 |
|
| 72 |
+
# model_to_use = "gemini/gemini-2.0-flash"
|
| 73 |
+
model_to_use = "openrouter/google/gemini-2.0-flash-lite-preview-02-05:free"
|
| 74 |
+
logger.info("Using {} for lyrics analysis", model_to_use)
|
| 75 |
|
| 76 |
# Use the function with retry mechanism
|
| 77 |
logger.info("Analyzing lyrics for song: '{}' by '{}'", song_title, artist)
|
| 78 |
+
response_text = make_api_call_with_retry(model_to_use, prompt)
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
# Parse the string response into a JSON object (dictionary)
|
| 82 |
+
logger.debug(f"Parsing JSON response for {song_title}")
|
| 83 |
+
response_json = json.loads(response_text)
|
| 84 |
+
return response_json
|
| 85 |
+
except json.JSONDecodeError as e:
|
| 86 |
+
logger.error(f"Failed to parse lyrics analysis response as JSON: {str(e)}")
|
| 87 |
+
# Return the raw text response if parsing fails
|
| 88 |
+
# This will likely cause an error in the formatting step,
|
| 89 |
+
# but at least we'll have the raw output for debugging
|
| 90 |
+
return response_text
|
| 91 |
|
tools/formatting_tools.py
CHANGED
|
@@ -3,6 +3,8 @@ Formatting tools for displaying analysis results with rich formatting.
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
import json
|
|
|
|
|
|
|
| 6 |
from loguru import logger
|
| 7 |
from smolagents import Tool
|
| 8 |
from rich.console import Console
|
|
@@ -25,205 +27,220 @@ class FormatAnalysisResultsTool(Tool):
|
|
| 25 |
}
|
| 26 |
output_type = "string"
|
| 27 |
|
| 28 |
-
def forward(self, analysis_json, pretty: bool = True) -> str:
|
| 29 |
"""
|
| 30 |
Formats the analysis results for better readability.
|
| 31 |
|
| 32 |
Args:
|
| 33 |
-
analysis_json: JSON
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
pretty: Whether to use rich formatting (True) or simple text formatting (False)
|
| 35 |
|
| 36 |
Returns:
|
| 37 |
A formatted string representation of the analysis
|
| 38 |
"""
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
-
# "mood": "The overall mood/emotion of the song",
|
| 44 |
-
# "sections_analysis": [
|
| 45 |
-
# {
|
| 46 |
-
# "section_type": "verse/chorus/bridge/etc.",
|
| 47 |
-
# "section_number": 1,
|
| 48 |
-
# "lines": ["line1", "line2", ...],
|
| 49 |
-
# "analysis": "Analysis of this section whith respect to the overall theme"
|
| 50 |
-
# },
|
| 51 |
-
# ...
|
| 52 |
-
# ],
|
| 53 |
-
# "conclusion": "The song vibes and concepts of the underlying meaning"
|
| 54 |
-
# }
|
| 55 |
-
try:
|
| 56 |
-
# Parse the JSON string into a Python dictionary if it's a string
|
| 57 |
-
if isinstance(analysis_json, str):
|
| 58 |
-
analysis = json.loads(analysis_json)
|
| 59 |
-
else:
|
| 60 |
-
analysis = analysis_json
|
| 61 |
-
|
| 62 |
-
if pretty:
|
| 63 |
-
# Rich formatting with the rich library
|
| 64 |
-
try:
|
| 65 |
-
# Create a string buffer to capture the rich output
|
| 66 |
-
console = Console(record=True, width=100)
|
| 67 |
-
|
| 68 |
-
# Create a custom theme for consistent styling
|
| 69 |
-
# Using direct style definitions instead of named styles
|
| 70 |
-
custom_theme = Theme({
|
| 71 |
-
"heading": "bold cyan underline",
|
| 72 |
-
"highlight": "bold yellow",
|
| 73 |
-
"positive": "green",
|
| 74 |
-
"negative": "red",
|
| 75 |
-
"neutral": "magenta",
|
| 76 |
-
"quote": "italic yellow",
|
| 77 |
-
"metadata": "dim white",
|
| 78 |
-
"conclusion": "bold magenta" # Add style for conclusion
|
| 79 |
-
})
|
| 80 |
-
|
| 81 |
-
# Apply the theme to our console
|
| 82 |
-
console.theme = custom_theme
|
| 83 |
-
|
| 84 |
-
# Format the summary section
|
| 85 |
-
summary = analysis.get("summary", "No summary available")
|
| 86 |
-
console.print(Panel(Text(summary, justify="center"), title="[heading]Song Analysis[/]", subtitle="[metadata]Summary[/]"))
|
| 87 |
-
|
| 88 |
-
# Create a table for the main themes and mood
|
| 89 |
-
info_table = Table(show_header=False, box=ROUNDED, expand=True)
|
| 90 |
-
info_table.add_column("Key", style="bold blue")
|
| 91 |
-
info_table.add_column("Value")
|
| 92 |
-
|
| 93 |
-
# Add the mood
|
| 94 |
-
mood = analysis.get("mood", "Not specified")
|
| 95 |
-
info_table.add_row("Mood", mood)
|
| 96 |
-
|
| 97 |
-
# Add the themes as a comma-separated list
|
| 98 |
-
themes = analysis.get("main_themes", [])
|
| 99 |
-
if themes:
|
| 100 |
-
themes_text = ", ".join([f"[highlight]{theme}[/]" for theme in themes])
|
| 101 |
-
info_table.add_row("Main Themes", themes_text)
|
| 102 |
-
|
| 103 |
-
console.print(info_table)
|
| 104 |
-
|
| 105 |
-
# Section-by-section analysis
|
| 106 |
-
sections = analysis.get("sections_analysis", [])
|
| 107 |
-
if sections:
|
| 108 |
-
console.print("\n[heading]Section-by-Section Analysis[/]")
|
| 109 |
-
|
| 110 |
-
for section in sections:
|
| 111 |
-
section_type = section.get("section_type", "Unknown")
|
| 112 |
-
section_number = section.get("section_number", "")
|
| 113 |
-
section_title = f"{section_type.title()} {section_number}" if section_number else section_type.title()
|
| 114 |
-
|
| 115 |
-
section_analysis = section.get("analysis", "No analysis available")
|
| 116 |
-
lines = section.get("lines", [])
|
| 117 |
-
|
| 118 |
-
# Create a group with the lyrics and analysis
|
| 119 |
-
section_content = []
|
| 120 |
-
|
| 121 |
-
if lines:
|
| 122 |
-
# Format lyrics in a more readable way
|
| 123 |
-
section_content.append(Text("Lyrics:", style="bold blue"))
|
| 124 |
-
# Форматируем каждую строку лирики с стилем quote
|
| 125 |
-
lyrics_lines = []
|
| 126 |
-
for line in lines:
|
| 127 |
-
lyrics_lines.append(f"[quote]{line}[/]")
|
| 128 |
-
|
| 129 |
-
lyrics_panel = Panel(
|
| 130 |
-
"\n".join(lyrics_lines),
|
| 131 |
-
border_style="blue",
|
| 132 |
-
padding=(1, 2)
|
| 133 |
-
)
|
| 134 |
-
section_content.append(lyrics_panel)
|
| 135 |
-
|
| 136 |
-
section_content.append(Text("Analysis:", style="bold blue"))
|
| 137 |
-
section_content.append(Text(section_analysis))
|
| 138 |
-
|
| 139 |
-
# Add the section panel
|
| 140 |
-
console.print(Panel(
|
| 141 |
-
Group(*section_content),
|
| 142 |
-
title=f"[bold cyan]{section_title}[/]",
|
| 143 |
-
border_style="cyan"
|
| 144 |
-
))
|
| 145 |
-
|
| 146 |
-
# We no longer have significant_lines in the new format
|
| 147 |
-
|
| 148 |
-
# Conclusion
|
| 149 |
-
conclusion = analysis.get("conclusion", "No conclusion available")
|
| 150 |
-
console.print("\n[heading]Conclusion[/]")
|
| 151 |
-
console.print(Panel(conclusion, border_style="magenta"))
|
| 152 |
-
|
| 153 |
-
# Export the rich text as a string
|
| 154 |
-
return console.export_text()
|
| 155 |
-
|
| 156 |
-
except Exception as e:
|
| 157 |
-
logger.error("Error in rich formatting: {}", str(e))
|
| 158 |
-
# Fall back to simple formatting if rich formatting fails
|
| 159 |
-
logger.info("Falling back to simple text formatting")
|
| 160 |
-
pretty = False
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
-
#
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
#
|
| 173 |
themes = analysis.get("main_themes", [])
|
|
|
|
| 174 |
if themes:
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
for theme in themes:
|
| 178 |
-
formatted_text.append(f"• {theme}")
|
| 179 |
-
formatted_text.append("")
|
| 180 |
|
| 181 |
-
|
| 182 |
-
mood = analysis.get("mood", "Not specified")
|
| 183 |
-
formatted_text.append("MOOD")
|
| 184 |
-
formatted_text.append("====")
|
| 185 |
-
formatted_text.append(mood)
|
| 186 |
-
formatted_text.append("")
|
| 187 |
|
| 188 |
-
#
|
| 189 |
sections = analysis.get("sections_analysis", [])
|
|
|
|
| 190 |
if sections:
|
| 191 |
-
|
| 192 |
-
formatted_text.append("==========================")
|
| 193 |
|
| 194 |
-
for
|
| 195 |
section_type = section.get("section_type", "Unknown")
|
| 196 |
-
section_number = section.get("section_number",
|
|
|
|
|
|
|
|
|
|
| 197 |
section_analysis = section.get("analysis", "No analysis available")
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
|
| 202 |
-
# Format the section lines
|
| 203 |
-
lines = section.get("lines", [])
|
| 204 |
if lines:
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
| 206 |
for line in lines:
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
# We no longer have significant_lines in the new format
|
| 215 |
|
| 216 |
# Conclusion
|
| 217 |
conclusion = analysis.get("conclusion", "No conclusion available")
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
import json
|
| 6 |
+
import traceback
|
| 7 |
+
from typing import Dict, Any, List, Union
|
| 8 |
from loguru import logger
|
| 9 |
from smolagents import Tool
|
| 10 |
from rich.console import Console
|
|
|
|
| 27 |
}
|
| 28 |
output_type = "string"
|
| 29 |
|
| 30 |
+
def forward(self, analysis_json: Dict, pretty: bool = True) -> str:
|
| 31 |
"""
|
| 32 |
Formats the analysis results for better readability.
|
| 33 |
|
| 34 |
Args:
|
| 35 |
+
analysis_json: Dics JSON containing the analysis results
|
| 36 |
+
# Format:
|
| 37 |
+
# {
|
| 38 |
+
# "summary": "Overall analysis of the song vibes, meaning and mood",
|
| 39 |
+
# "main_themes": ["theme1", "theme2", ...],
|
| 40 |
+
# "mood": "The overall mood/emotion of the song",
|
| 41 |
+
# "sections_analysis": [
|
| 42 |
+
# {
|
| 43 |
+
# "section_type": "verse/chorus/bridge/etc.",
|
| 44 |
+
# "section_number": 1,
|
| 45 |
+
# "lines": ["line1", "line2", ...],
|
| 46 |
+
# "analysis": "Analysis of this section whith respect to the overall theme"
|
| 47 |
+
# },
|
| 48 |
+
# ...
|
| 49 |
+
# ],
|
| 50 |
+
# "conclusion": "The song vibes and concepts of the underlying meaning"
|
| 51 |
+
# }
|
| 52 |
pretty: Whether to use rich formatting (True) or simple text formatting (False)
|
| 53 |
|
| 54 |
Returns:
|
| 55 |
A formatted string representation of the analysis
|
| 56 |
"""
|
| 57 |
+
|
| 58 |
+
analysis = analysis_json
|
| 59 |
+
# Log the structure of the parsed analysis
|
| 60 |
+
logger.debug(f"Analysis structure keys: {list(analysis.keys()) if isinstance(analysis, dict) else 'Not a dictionary'}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
if pretty:
|
| 63 |
+
# Rich formatting with the rich library
|
| 64 |
+
try:
|
| 65 |
+
logger.debug("Starting rich formatting")
|
| 66 |
+
# Create a console that outputs to a temporary file
|
| 67 |
+
import tempfile
|
| 68 |
+
temp_file = tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', delete=False)
|
| 69 |
+
console = Console(file=temp_file, width=100)
|
| 70 |
|
| 71 |
+
# Create a custom theme for consistent styling
|
| 72 |
+
# Using direct style definitions instead of named styles
|
| 73 |
+
custom_theme = Theme({
|
| 74 |
+
"heading": "bold cyan underline",
|
| 75 |
+
"highlight": "bold yellow",
|
| 76 |
+
"positive": "green",
|
| 77 |
+
"negative": "red",
|
| 78 |
+
"neutral": "magenta",
|
| 79 |
+
"quote": "italic yellow",
|
| 80 |
+
"metadata": "dim white",
|
| 81 |
+
"conclusion": "bold magenta" # Add style for conclusion
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
# Apply the theme to our console
|
| 85 |
+
console.theme = custom_theme
|
| 86 |
+
|
| 87 |
+
# Format the summary section
|
| 88 |
+
summary = analysis.get("summary", "No summary available")
|
| 89 |
+
logger.debug(f"Summary content: {summary if summary else 'Not available'}")
|
| 90 |
+
console.print(Panel(Text(summary, justify="center"), title="[heading]Song Analysis[/]", subtitle="[metadata]Summary[/]"))
|
| 91 |
+
|
| 92 |
+
# Create a table for the main themes and mood
|
| 93 |
+
info_table = Table(show_header=False, box=ROUNDED, expand=True)
|
| 94 |
+
info_table.add_column("Key", style="bold blue")
|
| 95 |
+
info_table.add_column("Value")
|
| 96 |
+
|
| 97 |
+
# Add the mood
|
| 98 |
+
mood = analysis.get("mood", "Not specified")
|
| 99 |
+
logger.debug(f"Mood content: {mood if mood else 'Not specified'}")
|
| 100 |
+
info_table.add_row("Mood", mood)
|
| 101 |
|
| 102 |
+
# Add the themes as a comma-separated list
|
| 103 |
themes = analysis.get("main_themes", [])
|
| 104 |
+
logger.debug(f"Themes: {themes if themes else 'Not available'}")
|
| 105 |
if themes:
|
| 106 |
+
themes_text = ", ".join([f"[highlight]{theme}[/]" for theme in themes])
|
| 107 |
+
info_table.add_row("Main Themes", themes_text)
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
console.print(info_table)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
# Section-by-section analysis
|
| 112 |
sections = analysis.get("sections_analysis", [])
|
| 113 |
+
logger.debug(f"Sections count: {len(sections) if sections else 0}")
|
| 114 |
if sections:
|
| 115 |
+
console.print("\n[heading]Section-by-Section Analysis[/]")
|
|
|
|
| 116 |
|
| 117 |
+
for section in sections:
|
| 118 |
section_type = section.get("section_type", "Unknown")
|
| 119 |
+
section_number = section.get("section_number", "")
|
| 120 |
+
logger.debug(f"Processing section: {section_type} {section_number}")
|
| 121 |
+
section_title = f"{section_type.title()} {section_number}" if section_number else section_type.title()
|
| 122 |
+
|
| 123 |
section_analysis = section.get("analysis", "No analysis available")
|
| 124 |
+
lines = section.get("lines", [])
|
| 125 |
+
logger.debug(f"Section lines count: {len(lines) if lines else 0}")
|
| 126 |
|
| 127 |
+
# Create a group with the lyrics and analysis
|
| 128 |
+
section_content = []
|
| 129 |
|
|
|
|
|
|
|
| 130 |
if lines:
|
| 131 |
+
# Format lyrics in a more readable way
|
| 132 |
+
section_content.append(Text("Lyrics:", style="bold blue"))
|
| 133 |
+
# Форматируем каждую строку лирики с стилем quote
|
| 134 |
+
lyrics_lines = []
|
| 135 |
for line in lines:
|
| 136 |
+
lyrics_lines.append(f"[quote]{line}[/]")
|
| 137 |
+
|
| 138 |
+
lyrics_panel = Panel(
|
| 139 |
+
"\n".join(lyrics_lines),
|
| 140 |
+
border_style="blue",
|
| 141 |
+
padding=(1, 2)
|
| 142 |
+
)
|
| 143 |
+
section_content.append(lyrics_panel)
|
| 144 |
|
| 145 |
+
section_content.append(Text("Analysis:", style="bold blue"))
|
| 146 |
+
section_content.append(Text(section_analysis))
|
| 147 |
+
|
| 148 |
+
# Add the section panel
|
| 149 |
+
console.print(Panel(
|
| 150 |
+
Group(*section_content),
|
| 151 |
+
title=f"[bold cyan]{section_title}[/]",
|
| 152 |
+
border_style="cyan"
|
| 153 |
+
))
|
| 154 |
|
| 155 |
# We no longer have significant_lines in the new format
|
| 156 |
|
| 157 |
# Conclusion
|
| 158 |
conclusion = analysis.get("conclusion", "No conclusion available")
|
| 159 |
+
logger.debug(f"Conclusion content: {conclusion if conclusion else 'Not available'}")
|
| 160 |
+
console.print("\n[heading]Conclusion[/]")
|
| 161 |
+
console.print(Panel(conclusion, border_style="magenta"))
|
| 162 |
|
| 163 |
+
# Save output to file and read back as string
|
| 164 |
+
temp_file.close()
|
| 165 |
+
with open(temp_file.name, 'r', encoding='utf-8') as f:
|
| 166 |
+
result = f.read()
|
| 167 |
+
# Clean up the temp file
|
| 168 |
+
import os
|
| 169 |
+
try:
|
| 170 |
+
os.unlink(temp_file.name)
|
| 171 |
+
except Exception as e:
|
| 172 |
+
logger.warning(f"Could not delete temporary file {temp_file.name}: {str(e)}")
|
| 173 |
+
return result
|
| 174 |
|
| 175 |
+
except Exception as e:
|
| 176 |
+
error_traceback = traceback.format_exc()
|
| 177 |
+
logger.error(f"Error in rich formatting: {str(e)}\nTraceback:\n{error_traceback}")
|
| 178 |
+
# Log the current state of the analysis object for debugging
|
| 179 |
+
if isinstance(analysis, dict):
|
| 180 |
+
for key, value in analysis.items():
|
| 181 |
+
logger.debug(f"Key: {key}, Value type: {type(value)}, Value: {str(value)[:100]}{'...' if len(str(value)) > 100 else ''}")
|
| 182 |
+
# Fall back to simple formatting if rich formatting fails
|
| 183 |
+
logger.info("Falling back to simple text formatting")
|
| 184 |
+
pretty = False
|
| 185 |
+
|
| 186 |
+
if not pretty:
|
| 187 |
+
# Simple text formatting
|
| 188 |
+
formatted_text = []
|
| 189 |
+
|
| 190 |
+
# Summary
|
| 191 |
+
formatted_text.append("SONG ANALYSIS SUMMARY")
|
| 192 |
+
formatted_text.append("====================")
|
| 193 |
+
formatted_text.append(analysis.get("summary", "No summary available"))
|
| 194 |
+
formatted_text.append("")
|
| 195 |
+
|
| 196 |
+
# Main themes
|
| 197 |
+
themes = analysis.get("main_themes", [])
|
| 198 |
+
if themes:
|
| 199 |
+
formatted_text.append("MAIN THEMES")
|
| 200 |
+
formatted_text.append("===========")
|
| 201 |
+
for theme in themes:
|
| 202 |
+
formatted_text.append(f"• {theme}")
|
| 203 |
+
formatted_text.append("")
|
| 204 |
+
|
| 205 |
+
# Mood
|
| 206 |
+
mood = analysis.get("mood", "Not specified")
|
| 207 |
+
formatted_text.append("MOOD")
|
| 208 |
+
formatted_text.append("====")
|
| 209 |
+
formatted_text.append(mood)
|
| 210 |
+
formatted_text.append("")
|
| 211 |
+
|
| 212 |
+
# Sections analysis
|
| 213 |
+
sections = analysis.get("sections_analysis", [])
|
| 214 |
+
if sections:
|
| 215 |
+
formatted_text.append("SECTION-BY-SECTION ANALYSIS")
|
| 216 |
+
formatted_text.append("==========================")
|
| 217 |
+
|
| 218 |
+
for i, section in enumerate(sections):
|
| 219 |
+
section_type = section.get("section_type", "Unknown")
|
| 220 |
+
section_number = section.get("section_number", i+1)
|
| 221 |
+
section_analysis = section.get("analysis", "No analysis available")
|
| 222 |
+
|
| 223 |
+
formatted_text.append(f"{section_type.upper()} {section_number}")
|
| 224 |
+
formatted_text.append("-" * (len(section_type) + len(str(section_number)) + 1))
|
| 225 |
+
|
| 226 |
+
# Format the section lines
|
| 227 |
+
lines = section.get("lines", [])
|
| 228 |
+
if lines:
|
| 229 |
+
formatted_text.append("Lyrics:")
|
| 230 |
+
for line in lines:
|
| 231 |
+
formatted_text.append(f"> {line}")
|
| 232 |
+
formatted_text.append("")
|
| 233 |
+
|
| 234 |
+
formatted_text.append("Analysis:")
|
| 235 |
+
formatted_text.append(section_analysis)
|
| 236 |
+
formatted_text.append("")
|
| 237 |
+
|
| 238 |
+
# We no longer have significant_lines in the new format
|
| 239 |
+
|
| 240 |
+
# Conclusion
|
| 241 |
+
conclusion = analysis.get("conclusion", "No conclusion available")
|
| 242 |
+
formatted_text.append("CONCLUSION")
|
| 243 |
+
formatted_text.append("==========")
|
| 244 |
+
formatted_text.append(conclusion)
|
| 245 |
+
|
| 246 |
+
return "\n".join(formatted_text)
|
tools/search_tools.py
CHANGED
|
@@ -2,11 +2,83 @@
|
|
| 2 |
Search tools for finding song lyrics and related information.
|
| 3 |
"""
|
| 4 |
|
| 5 |
-
import
|
| 6 |
import random
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
from loguru import logger
|
| 9 |
-
from smolagents import DuckDuckGoSearchTool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class ThrottledDuckDuckGoSearchTool(DuckDuckGoSearchTool):
|
|
|
|
| 2 |
Search tools for finding song lyrics and related information.
|
| 3 |
"""
|
| 4 |
|
| 5 |
+
import os
|
| 6 |
import random
|
| 7 |
+
import time
|
| 8 |
+
from typing import Any, Dict, List
|
| 9 |
+
|
| 10 |
+
import requests
|
| 11 |
from loguru import logger
|
| 12 |
+
from smolagents import DuckDuckGoSearchTool, Tool
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class BraveSearchTool(Tool):
|
| 16 |
+
"""
|
| 17 |
+
A tool for performing web searches using the Brave Search API.
|
| 18 |
+
|
| 19 |
+
This tool requires a Brave Search API key to be set in the environment
|
| 20 |
+
variable BRAVE_API_KEY or passed directly to the constructor.
|
| 21 |
+
|
| 22 |
+
Documentation: https://api.search.brave.com/app/documentation
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, max_results: int = 10, **kwargs):
|
| 26 |
+
"""
|
| 27 |
+
Initialize the Brave Search tool.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
max_results: Maximum number of results to return (default: 10)
|
| 31 |
+
"""
|
| 32 |
+
super().__init__(**kwargs)
|
| 33 |
+
self.api_key = os.environ.get("BRAVE_API_KEY")
|
| 34 |
+
if not self.api_key:
|
| 35 |
+
logger.warning("No Brave API key found. Set BRAVE_API_KEY environment variable or pass api_key parameter.")
|
| 36 |
+
self.max_results = max_results
|
| 37 |
+
self.name = "brave_search"
|
| 38 |
+
self.description = "Search the web using Brave Search API"
|
| 39 |
+
self.inputs = {"query": {"type": "string", "description": "The search query string"}}
|
| 40 |
+
self.output_type = "string"
|
| 41 |
+
logger.info(f"Initialized BraveSearchTool with max_results={max_results}")
|
| 42 |
+
|
| 43 |
+
def forward(self, query: str) -> List[Dict[str, Any]]:
|
| 44 |
+
"""
|
| 45 |
+
Execute a search using the Brave Search API.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
query: The search query string
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
List of search results in the format:
|
| 52 |
+
[{"title": str, "href": str, "body": str}, ...]
|
| 53 |
+
"""
|
| 54 |
+
if not self.api_key:
|
| 55 |
+
logger.error("Brave Search API key is not set")
|
| 56 |
+
return [{"title": "API Key Error", "href": "", "body": "Brave Search API key is not set"}]
|
| 57 |
+
|
| 58 |
+
url = "https://api.search.brave.com/res/v1/web/search"
|
| 59 |
+
headers = {"Accept": "application/json", "X-Subscription-Token": self.api_key}
|
| 60 |
+
params = {"q": query, "count": self.max_results}
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
logger.info(f"Performing Brave search for query: '{query}'")
|
| 64 |
+
response = requests.get(url, headers=headers, params=params)
|
| 65 |
+
response.raise_for_status()
|
| 66 |
+
data = response.json()
|
| 67 |
+
|
| 68 |
+
results = []
|
| 69 |
+
if "web" in data and "results" in data["web"]:
|
| 70 |
+
for result in data["web"]["results"]:
|
| 71 |
+
results.append({
|
| 72 |
+
"title": result.get("title", ""),
|
| 73 |
+
"href": result.get("url", ""),
|
| 74 |
+
"body": result.get("description", "")
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
logger.info(f"Found {len(results)} results for query: '{query}'")
|
| 78 |
+
return results
|
| 79 |
+
except Exception as e:
|
| 80 |
+
logger.error(f"Error in Brave search: {str(e)}")
|
| 81 |
+
return [{"title": "Search error", "href": "", "body": f"Error performing search: {str(e)}"}]
|
| 82 |
|
| 83 |
|
| 84 |
class ThrottledDuckDuckGoSearchTool(DuckDuckGoSearchTool):
|