File size: 7,715 Bytes
05a3ce6
013068f
 
 
 
05a3ce6
 
 
 
 
 
 
 
 
013068f
05a3ce6
de58e79
013068f
 
 
 
 
 
 
 
05a3ce6
de58e79
013068f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05a3ce6
 
 
 
013068f
05a3ce6
013068f
05a3ce6
 
013068f
05a3ce6
 
 
de58e79
013068f
05a3ce6
 
 
de58e79
013068f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de58e79
013068f
 
 
 
 
 
 
 
 
 
 
 
05a3ce6
013068f
05a3ce6
013068f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05a3ce6
013068f
 
 
 
05a3ce6
013068f
 
 
 
 
 
 
 
 
 
 
 
de58e79
 
013068f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import os
import time
import random
import traceback
from loguru import logger
from Gradio_UI import GradioUI
from litellm import completion
from smolagents import (
    CodeAgent,
    DuckDuckGoSearchTool,
    FinalAnswerTool,
    LiteLLMModel,
    VisitWebpageTool,
    tool,
    Tool,
)

# Setting up logging with loguru - only terminal output
logger.remove()  # Remove default handlers
logger.add(
    lambda msg: print(msg, end=""), 
    level="INFO", 
    format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>")

# API key configuration
os.environ["GEMINI_API_KEY"] = os.getenv("GEMINI_API_KEY")


class LyricsSearchTool(Tool):
    """
    Uses web search to find song lyrics based on song title and artist name

    The search query should include the song title and artist name. The tool
    will return the lyrics of the song if found.

    Parameters
    ----------
    query : str
        The search query for finding song lyrics. Should include song title and artist name.

    Returns
    -------
    str
        The lyrics of the song if found, otherwise an empty string.
    """
    name = "lyrics_search_tool"
    description = "Uses web search to find song lyrics based on song title and artist name"
    inputs = {
        "query": {
            "type": "string",
            "description": "The search query for finding song lyrics. Should include song title and artist name.",
        }
    }
    output_type = "string"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def forward(self, query: str) -> str:
        assert isinstance(query, str), "Your search query must be a string"
        # TODO: Implement lyrics search functionality
        return "Lyrics search not implemented yet"


@tool
def analyze_lyrics_tool(song_title: str, artist: str, lyrics: str) -> str:
    """
    Performs a deep analysis of the musical track, given its metadata.

    Args:
        song_title: Title of the song or music track.
        artist: The name of the artist.
        lyrics: The lyrics of the song.

    Returns:
        A summary of the song's meaning in English.
    """

    prompt = f"""You are an expert in songs and their meanings. 
    Summarize the meaning of {song_title} by {artist} and identify 
    key themes based on the lyrics: 
    {lyrics}. 

    Include deep idea and vibes analysis with explanations 
    based on references to the exact lines.
    """

    # If the USE_ANTHROPIC environment variable is defined, use the Claude model
    if os.getenv("USE_ANTHROPIC", "false").lower() == "true":
        model_to_use = "claude-3-haiku-20240307"
        logger.info("Using Anthropic model: {} for lyrics analysis", model_to_use)
    else:
        model_to_use = "gemini/gemini-2.0-flash"
        logger.info("Using Gemini model: {} for lyrics analysis", model_to_use)

    # Use the function with retry mechanism
    logger.info("Analyzing lyrics for song: '{}' by '{}'", song_title, artist)
    return _make_api_call_with_retry(model_to_use, prompt)


# Function with manual implementation of retry mechanism
def _make_api_call_with_retry(model: str, prompt: str) -> str:
    """
    Makes an API call with a retry mechanism for error handling.

    Args:
        model: The model identifier to use.
        prompt: The prompt text to send to the model.

    Returns:
        The response from the model as a string.
    """
    max_attempts = 20
    base_delay = 10
    max_delay = 60
    attempt = 0
    last_exception = None
    
    while attempt < max_attempts:
        try:
            # Add a small random delay to prevent simultaneous requests
            jitter = random.uniform(0.1, 1.0)
            time.sleep(jitter)
            
            # If this is a retry attempt, add exponential backoff delay
            if attempt > 0:
                delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
                time.sleep(delay)
            
            response = completion(
                model=model,
                messages=[{"role": "user", "content": prompt}],
                num_retries=2,  # Built-in retry mechanism of LiteLLM
            )

            # Try to extract the content from the response
            try:
                analysis_result = response.choices[0].message.content.strip()
                return analysis_result
            except (AttributeError, KeyError, IndexError):
                try:
                    analysis_result = response["choices"][0]["message"]["content"].strip()
                    return analysis_result
                except (AttributeError, KeyError, IndexError):
                    # If we couldn't extract the content, return an error
                    raise ValueError("Failed to extract content from response")
                    
        except (ConnectionError, TimeoutError) as e:
            last_exception = e
            logger.warning("API call failed (attempt {}/{}) for model {}: {}. Retrying...", attempt+1, max_attempts, model, str(e))
            attempt += 1
            continue
        except Exception as e:
            logger.error("Unexpected error: {}", str(e))
            logger.error(traceback.format_exc())
            raise  # For other exceptions, we don't retry
    
    # If all attempts failed, re-raise the last exception
    if last_exception:
        logger.error("All {} attempts failed. Last error: {}", max_attempts, str(last_exception))
        raise last_exception


# TODO: use DuckDuckGoSearchTool to find related information
# for explanation in case the LLM itself is not confident or doesn't know
#

# Check if we need to use Anthropic for local testing
use_anthropic = os.getenv("USE_ANTHROPIC", "false").lower() == "true"

# Configure Anthropic API key if needed
if use_anthropic:
    os.environ["ANTHROPIC_API_KEY"] = os.getenv("ANTHROPIC_API_KEY")
    model = LiteLLMModel(model_id="claude-3-haiku-20240307")
    logger.info("Using Anthropic Claude model for local testing")
else:
    model = LiteLLMModel(model_id="gemini/gemini-2.0-flash")
    logger.info("Using Gemini model as default")

web_agent = CodeAgent(
    model=model,
    tools=[DuckDuckGoSearchTool(), VisitWebpageTool()],
    name="lyrics_search_agent",
    description="Browses the web to find original full lyrics and scrape them. Excels at building effective search queries",
    additional_authorized_imports=["numpy", "bs4"],
    max_steps=22,
    verbosity_level=2,
)


analysis_agent = CodeAgent(
    model=model,
    tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), analyze_lyrics_tool],
    name="lyrics_analysis_agent",
    description="You are a Song Analysis Expert with deep knowledge of music theory, lyrical interpretation, cultural contexts, and music history. Your role is to analyze song lyrics to uncover their deeper meaning, artistic significance, and historical context.",
    additional_authorized_imports=["numpy", "bs4"],
    max_steps=50,
    verbosity_level=2,
)


# When using the DuckDuckGoSearchTool, clearly indicate when information comes from external research versus your own knowledge base.
manager_agent = CodeAgent(
    model=model,
    tools=[FinalAnswerTool()],
    name="manager_agent",
    description="Manages the search process and coordinates the search and analysis of song lyrics.",
    managed_agents=[web_agent, analysis_agent],
    additional_authorized_imports=["json"],
    planning_interval=5,
    verbosity_level=2,
    max_steps=15,
)

logger.info("Initializing Gradio UI and launching server")
GradioUI(manager_agent).launch(
    debug=True, share=False, server_name="127.0.0.1", server_port=3000
)
logger.success("Server started successfully")