Complete Migration
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -27,7 +27,7 @@ AVAILABLE_MODELS: Dict[str, str] = {
|
|||||||
"Gemini 2.5 Flash": "openrouter/google/gemini-2.5-flash",
|
"Gemini 2.5 Flash": "openrouter/google/gemini-2.5-flash",
|
||||||
"Gemini 2.5 Flash Lite": "openrouter/google/gemini-2.5-flash-lite",
|
"Gemini 2.5 Flash Lite": "openrouter/google/gemini-2.5-flash-lite",
|
||||||
"Gemini 2.5 Pro": "openrouter/google/gemini-2.5-pro",
|
"Gemini 2.5 Pro": "openrouter/google/gemini-2.5-pro",
|
||||||
"GPT-5": "openrouter/openai/gpt-5"
|
"GPT-5": "openrouter/openai/gpt-5",
|
||||||
}
|
}
|
||||||
|
|
||||||
# openrouter API configuration
|
# openrouter API configuration
|
||||||
@@ -45,7 +45,7 @@ DEFAULT_CATEGORIES: List[str] = [
|
|||||||
"Engagement potential - how likely users are to like, retweet, or reply",
|
"Engagement potential - how likely users are to like, retweet, or reply",
|
||||||
"Clarity and readability - how easy the tweet is to understand",
|
"Clarity and readability - how easy the tweet is to understand",
|
||||||
"Emotional impact - how well the tweet evokes feelings or reactions",
|
"Emotional impact - how well the tweet evokes feelings or reactions",
|
||||||
"Relevance to target audience - how well it resonates with intended readers"
|
"Relevance to target audience - how well it resonates with intended readers",
|
||||||
]
|
]
|
||||||
|
|
||||||
# error messages
|
# error messages
|
||||||
|
|||||||
@@ -18,16 +18,15 @@ def format_evaluation_for_generator(evaluation: Optional[EvaluationResult]) -> s
|
|||||||
|
|
||||||
eval_lines = []
|
eval_lines = []
|
||||||
for eval in evaluation.evaluations:
|
for eval in evaluation.evaluations:
|
||||||
eval_lines.append(f"{eval.category} (Score: {eval.score}/{MAX_SCORE}): {eval.reasoning}")
|
eval_lines.append(
|
||||||
|
f"{eval.category} (Score: {eval.score}/{MAX_SCORE}): {eval.reasoning}"
|
||||||
|
)
|
||||||
|
|
||||||
return "\n".join(eval_lines)
|
return "\n".join(eval_lines)
|
||||||
|
|
||||||
|
|
||||||
def build_settings_dict(
|
def build_settings_dict(
|
||||||
selected_model: str,
|
selected_model: str, iterations: int, patience: int, use_cache: bool
|
||||||
iterations: int,
|
|
||||||
patience: int,
|
|
||||||
use_cache: bool
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Build a settings dictionary for saving.
|
Build a settings dictionary for saving.
|
||||||
@@ -45,7 +44,7 @@ def build_settings_dict(
|
|||||||
"selected_model": selected_model,
|
"selected_model": selected_model,
|
||||||
"iterations": iterations,
|
"iterations": iterations,
|
||||||
"patience": patience,
|
"patience": patience,
|
||||||
"use_cache": use_cache
|
"use_cache": use_cache,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from .models import EvaluationResult
|
|||||||
from .modules import TweetGeneratorModule, TweetEvaluatorModule
|
from .modules import TweetGeneratorModule, TweetEvaluatorModule
|
||||||
from .helpers import format_evaluation_for_generator
|
from .helpers import format_evaluation_for_generator
|
||||||
|
|
||||||
|
|
||||||
class HillClimbingOptimizer:
|
class HillClimbingOptimizer:
|
||||||
"""Hill climbing optimizer for tweet improvement."""
|
"""Hill climbing optimizer for tweet improvement."""
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ class HillClimbingOptimizer:
|
|||||||
evaluator: TweetEvaluatorModule,
|
evaluator: TweetEvaluatorModule,
|
||||||
categories: List[str],
|
categories: List[str],
|
||||||
max_iterations: int = 10,
|
max_iterations: int = 10,
|
||||||
patience: int = 5
|
patience: int = 5,
|
||||||
):
|
):
|
||||||
self.generator = generator
|
self.generator = generator
|
||||||
self.evaluator = evaluator
|
self.evaluator = evaluator
|
||||||
@@ -20,7 +21,11 @@ class HillClimbingOptimizer:
|
|||||||
self.max_iterations = max_iterations
|
self.max_iterations = max_iterations
|
||||||
self.patience = patience
|
self.patience = patience
|
||||||
|
|
||||||
def optimize(self, initial_text: str) -> Iterator[Tuple[str, EvaluationResult, bool, int, Dict[str, str], Dict[str, str]]]:
|
def optimize(
|
||||||
|
self, initial_text: str
|
||||||
|
) -> Iterator[
|
||||||
|
Tuple[str, EvaluationResult, bool, int, Dict[str, str], Dict[str, str]]
|
||||||
|
]:
|
||||||
"""
|
"""
|
||||||
Optimize tweet using hill climbing algorithm.
|
Optimize tweet using hill climbing algorithm.
|
||||||
|
|
||||||
@@ -31,31 +36,36 @@ class HillClimbingOptimizer:
|
|||||||
generator_inputs = {
|
generator_inputs = {
|
||||||
"input_text": initial_text,
|
"input_text": initial_text,
|
||||||
"current_tweet": "",
|
"current_tweet": "",
|
||||||
"previous_evaluation": ""
|
"previous_evaluation": "",
|
||||||
}
|
}
|
||||||
current_tweet = self.generator(
|
current_tweet = self.generator(
|
||||||
input_text=initial_text,
|
input_text=initial_text, current_tweet="", previous_evaluation=None
|
||||||
current_tweet="",
|
|
||||||
previous_evaluation=None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
evaluator_inputs = {
|
evaluator_inputs = {
|
||||||
"original_text": initial_text,
|
"original_text": initial_text,
|
||||||
"current_best_tweet": "",
|
"current_best_tweet": "",
|
||||||
"tweet_text": current_tweet
|
"tweet_text": current_tweet,
|
||||||
}
|
}
|
||||||
current_score = self.evaluator(
|
current_score = self.evaluator(
|
||||||
tweet_text=current_tweet,
|
tweet_text=current_tweet,
|
||||||
categories=self.categories,
|
categories=self.categories,
|
||||||
original_text=initial_text,
|
original_text=initial_text,
|
||||||
current_best_tweet=""
|
current_best_tweet="",
|
||||||
)
|
)
|
||||||
|
|
||||||
best_tweet = current_tweet
|
best_tweet = current_tweet
|
||||||
best_score = current_score
|
best_score = current_score
|
||||||
patience_counter = 0
|
patience_counter = 0
|
||||||
|
|
||||||
yield (current_tweet, current_score, True, patience_counter, generator_inputs, evaluator_inputs)
|
yield (
|
||||||
|
current_tweet,
|
||||||
|
current_score,
|
||||||
|
True,
|
||||||
|
patience_counter,
|
||||||
|
generator_inputs,
|
||||||
|
evaluator_inputs,
|
||||||
|
)
|
||||||
|
|
||||||
for iteration in range(1, self.max_iterations):
|
for iteration in range(1, self.max_iterations):
|
||||||
# Generate improved tweet with previous evaluation as feedback
|
# Generate improved tweet with previous evaluation as feedback
|
||||||
@@ -66,26 +76,26 @@ class HillClimbingOptimizer:
|
|||||||
generator_inputs = {
|
generator_inputs = {
|
||||||
"input_text": initial_text,
|
"input_text": initial_text,
|
||||||
"current_tweet": best_tweet,
|
"current_tweet": best_tweet,
|
||||||
"previous_evaluation": eval_text
|
"previous_evaluation": eval_text,
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate_tweet = self.generator(
|
candidate_tweet = self.generator(
|
||||||
input_text=initial_text,
|
input_text=initial_text,
|
||||||
current_tweet=best_tweet,
|
current_tweet=best_tweet,
|
||||||
previous_evaluation=best_score
|
previous_evaluation=best_score,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Evaluate candidate
|
# Evaluate candidate
|
||||||
evaluator_inputs = {
|
evaluator_inputs = {
|
||||||
"original_text": initial_text,
|
"original_text": initial_text,
|
||||||
"current_best_tweet": best_tweet,
|
"current_best_tweet": best_tweet,
|
||||||
"tweet_text": candidate_tweet
|
"tweet_text": candidate_tweet,
|
||||||
}
|
}
|
||||||
candidate_score = self.evaluator(
|
candidate_score = self.evaluator(
|
||||||
tweet_text=candidate_tweet,
|
tweet_text=candidate_tweet,
|
||||||
categories=self.categories,
|
categories=self.categories,
|
||||||
original_text=initial_text,
|
original_text=initial_text,
|
||||||
current_best_tweet=best_tweet
|
current_best_tweet=best_tweet,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if candidate is better (hill climbing condition)
|
# Check if candidate is better (hill climbing condition)
|
||||||
@@ -95,10 +105,24 @@ class HillClimbingOptimizer:
|
|||||||
best_tweet = candidate_tweet
|
best_tweet = candidate_tweet
|
||||||
best_score = candidate_score
|
best_score = candidate_score
|
||||||
patience_counter = 0
|
patience_counter = 0
|
||||||
yield (candidate_tweet, candidate_score, True, patience_counter, generator_inputs, evaluator_inputs)
|
yield (
|
||||||
|
candidate_tweet,
|
||||||
|
candidate_score,
|
||||||
|
True,
|
||||||
|
patience_counter,
|
||||||
|
generator_inputs,
|
||||||
|
evaluator_inputs,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
patience_counter += 1
|
patience_counter += 1
|
||||||
yield (best_tweet, candidate_score, False, patience_counter, generator_inputs, evaluator_inputs)
|
yield (
|
||||||
|
best_tweet,
|
||||||
|
candidate_score,
|
||||||
|
False,
|
||||||
|
patience_counter,
|
||||||
|
generator_inputs,
|
||||||
|
evaluator_inputs,
|
||||||
|
)
|
||||||
|
|
||||||
# Early stopping if no improvement for 'patience' iterations
|
# Early stopping if no improvement for 'patience' iterations
|
||||||
if patience_counter >= self.patience:
|
if patience_counter >= self.patience:
|
||||||
@@ -110,10 +134,16 @@ class HillClimbingOptimizer:
|
|||||||
evaluator_inputs = {
|
evaluator_inputs = {
|
||||||
"original_text": initial_text,
|
"original_text": initial_text,
|
||||||
"current_best_tweet": best_tweet,
|
"current_best_tweet": best_tweet,
|
||||||
"tweet_text": best_tweet
|
"tweet_text": best_tweet,
|
||||||
}
|
}
|
||||||
yield (best_tweet, best_score, False, patience_counter, generator_inputs, evaluator_inputs)
|
yield (
|
||||||
|
best_tweet,
|
||||||
|
best_score,
|
||||||
|
False,
|
||||||
|
patience_counter,
|
||||||
|
generator_inputs,
|
||||||
|
evaluator_inputs,
|
||||||
|
)
|
||||||
|
|
||||||
if patience_counter >= self.patience:
|
if patience_counter >= self.patience:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
105
agent/index.py
Normal file
105
agent/index.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from modaic import PrecompiledAgent, PrecompiledConfig
|
||||||
|
from .modules import TweetGeneratorModule, TweetEvaluatorModule
|
||||||
|
from .models import EvaluationResult
|
||||||
|
from .hill_climbing import HillClimbingOptimizer
|
||||||
|
from typing import Optional, List
|
||||||
|
from .utils import get_dspy_lm
|
||||||
|
from .constants import DEFAULT_CATEGORIES, DEFAULT_ITERATIONS, DEFAULT_PATIENCE
|
||||||
|
|
||||||
|
|
||||||
|
class TweetOptimizerConfig(PrecompiledConfig):
|
||||||
|
lm: str = "openrouter/google/gemini-2.5-flash"
|
||||||
|
eval_lm: str = "openrouter/openai/gpt-5"
|
||||||
|
categories: List[str] = DEFAULT_CATEGORIES
|
||||||
|
max_iterations: int = DEFAULT_ITERATIONS
|
||||||
|
patience: int = DEFAULT_PATIENCE
|
||||||
|
|
||||||
|
|
||||||
|
class TweetOptimizerAgent(PrecompiledAgent):
|
||||||
|
config: TweetOptimizerConfig
|
||||||
|
|
||||||
|
current_tweet: str = ""
|
||||||
|
previous_evaluation: Optional[EvaluationResult] = None
|
||||||
|
|
||||||
|
def __init__(self, config: TweetOptimizerConfig, **kwargs):
|
||||||
|
super().__init__(config, **kwargs)
|
||||||
|
self.tweet_generator = TweetGeneratorModule()
|
||||||
|
self.tweet_evaluator = TweetEvaluatorModule()
|
||||||
|
|
||||||
|
# set up optimizer
|
||||||
|
self.optimizer = HillClimbingOptimizer(
|
||||||
|
generator=self.tweet_generator,
|
||||||
|
evaluator=self.tweet_evaluator,
|
||||||
|
categories=config.categories,
|
||||||
|
max_iterations=config.max_iterations,
|
||||||
|
patience=config.patience,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.lm = config.lm
|
||||||
|
self.eval_lm = config.eval_lm
|
||||||
|
self.categories = config.categories
|
||||||
|
self.max_iterations = config.max_iterations
|
||||||
|
self.patience = config.patience
|
||||||
|
|
||||||
|
# initialize DSPy with the specified model
|
||||||
|
self.tweet_generator.set_lm(get_dspy_lm(config.lm))
|
||||||
|
self.tweet_evaluator.set_lm(get_dspy_lm(config.eval_lm))
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
input_text: str,
|
||||||
|
iterations: Optional[int] = None,
|
||||||
|
patience: Optional[int] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Run full optimization process."""
|
||||||
|
max_iterations = iterations or self.max_iterations
|
||||||
|
patience_limit = patience or self.patience
|
||||||
|
|
||||||
|
results = {
|
||||||
|
"initial_text": input_text,
|
||||||
|
"final_tweet": "",
|
||||||
|
"best_score": 0.0,
|
||||||
|
"iterations_run": 0,
|
||||||
|
"early_stopped": False,
|
||||||
|
"scores_history": [],
|
||||||
|
"improvement_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
best_tweet = ""
|
||||||
|
best_score = 0.0
|
||||||
|
|
||||||
|
for iteration, (
|
||||||
|
current_tweet,
|
||||||
|
scores,
|
||||||
|
is_improvement,
|
||||||
|
patience_counter,
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) in enumerate(self.optimizer.optimize(input_text)):
|
||||||
|
iteration_num = iteration + 1
|
||||||
|
results["iterations_run"] = iteration_num
|
||||||
|
results["scores_history"].append(scores)
|
||||||
|
|
||||||
|
if is_improvement:
|
||||||
|
best_tweet = current_tweet
|
||||||
|
best_score = sum(scores.category_scores) / len(scores.category_scores)
|
||||||
|
results["improvement_count"] += 1
|
||||||
|
|
||||||
|
# check for early stopping
|
||||||
|
if patience_counter >= patience_limit:
|
||||||
|
results["early_stopped"] = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# stop at max iterations
|
||||||
|
if iteration_num >= max_iterations:
|
||||||
|
break
|
||||||
|
|
||||||
|
results.update({"final_tweet": best_tweet, "best_score": best_score})
|
||||||
|
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.current_tweet = ""
|
||||||
|
self.previous_evaluation = None
|
||||||
@@ -2,6 +2,7 @@ from pydantic import BaseModel, Field, validator
|
|||||||
from typing import List
|
from typing import List
|
||||||
from .constants import MIN_SCORE, MAX_SCORE
|
from .constants import MIN_SCORE, MAX_SCORE
|
||||||
|
|
||||||
|
|
||||||
class CategoryEvaluation(BaseModel):
|
class CategoryEvaluation(BaseModel):
|
||||||
"""Pydantic model for a single category evaluation with reasoning."""
|
"""Pydantic model for a single category evaluation with reasoning."""
|
||||||
|
|
||||||
@@ -10,16 +11,19 @@ class CategoryEvaluation(BaseModel):
|
|||||||
score: int = Field(
|
score: int = Field(
|
||||||
description=f"Score for this category ({MIN_SCORE}-{MAX_SCORE})",
|
description=f"Score for this category ({MIN_SCORE}-{MAX_SCORE})",
|
||||||
ge=MIN_SCORE,
|
ge=MIN_SCORE,
|
||||||
le=MAX_SCORE
|
le=MAX_SCORE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@validator('score')
|
@validator("score")
|
||||||
def validate_score(cls, score):
|
def validate_score(cls, score):
|
||||||
"""Ensure score is within the valid range."""
|
"""Ensure score is within the valid range."""
|
||||||
if not isinstance(score, int) or score < MIN_SCORE or score > MAX_SCORE:
|
if not isinstance(score, int) or score < MIN_SCORE or score > MAX_SCORE:
|
||||||
raise ValueError(f"Score {score} must be an integer between {MIN_SCORE} and {MAX_SCORE}")
|
raise ValueError(
|
||||||
|
f"Score {score} must be an integer between {MIN_SCORE} and {MAX_SCORE}"
|
||||||
|
)
|
||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
class EvaluationResult(BaseModel):
|
class EvaluationResult(BaseModel):
|
||||||
"""Pydantic model for tweet evaluation results."""
|
"""Pydantic model for tweet evaluation results."""
|
||||||
|
|
||||||
@@ -27,7 +31,7 @@ class EvaluationResult(BaseModel):
|
|||||||
description="List of category evaluations with reasoning and scores"
|
description="List of category evaluations with reasoning and scores"
|
||||||
)
|
)
|
||||||
|
|
||||||
@validator('evaluations')
|
@validator("evaluations")
|
||||||
def validate_evaluations(cls, evals):
|
def validate_evaluations(cls, evals):
|
||||||
"""Ensure we have at least one evaluation."""
|
"""Ensure we have at least one evaluation."""
|
||||||
if not evals or len(evals) < 1:
|
if not evals or len(evals) < 1:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dspy
|
import dspy
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from models import EvaluationResult, CategoryEvaluation
|
from .models import EvaluationResult, CategoryEvaluation
|
||||||
from constants import (
|
from .constants import (
|
||||||
TWEET_MAX_LENGTH,
|
TWEET_MAX_LENGTH,
|
||||||
TWEET_TRUNCATION_SUFFIX,
|
TWEET_TRUNCATION_SUFFIX,
|
||||||
DEFAULT_SCORE,
|
DEFAULT_SCORE,
|
||||||
@@ -10,29 +10,44 @@ from constants import (
|
|||||||
ERROR_GENERATION,
|
ERROR_GENERATION,
|
||||||
ERROR_EVALUATION,
|
ERROR_EVALUATION,
|
||||||
MIN_SCORE,
|
MIN_SCORE,
|
||||||
MAX_SCORE
|
MAX_SCORE,
|
||||||
)
|
)
|
||||||
from helpers import format_evaluation_for_generator, truncate_tweet
|
from .helpers import format_evaluation_for_generator, truncate_tweet
|
||||||
|
|
||||||
|
|
||||||
class TweetGenerator(dspy.Signature):
|
class TweetGenerator(dspy.Signature):
|
||||||
"""Generate or improve a tweet based on input text and detailed evaluation feedback with reasoning."""
|
"""Generate or improve a tweet based on input text and detailed evaluation feedback with reasoning."""
|
||||||
|
|
||||||
input_text: str = dspy.InputField(desc="Original text or current tweet to improve")
|
input_text: str = dspy.InputField(desc="Original text or current tweet to improve")
|
||||||
current_tweet: str = dspy.InputField(desc="Current best tweet version (empty for first generation)")
|
current_tweet: str = dspy.InputField(
|
||||||
previous_evaluation: str = dspy.InputField(desc="Previous evaluation with category-by-category reasoning and scores (empty for first generation)")
|
desc="Current best tweet version (empty for first generation)"
|
||||||
improved_tweet: str = dspy.OutputField(desc=f"Generated or improved tweet text (max {TWEET_MAX_LENGTH} characters)")
|
)
|
||||||
|
previous_evaluation: str = dspy.InputField(
|
||||||
|
desc="Previous evaluation with category-by-category reasoning and scores (empty for first generation)"
|
||||||
|
)
|
||||||
|
improved_tweet: str = dspy.OutputField(
|
||||||
|
desc=f"Generated or improved tweet text (max {TWEET_MAX_LENGTH} characters)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TweetEvaluator(dspy.Signature):
|
class TweetEvaluator(dspy.Signature):
|
||||||
"""Evaluate a tweet across multiple custom categories. For each category, provide detailed reasoning explaining the score, then assign a score. Ensure the tweet maintains the same meaning as the original text."""
|
"""Evaluate a tweet across multiple custom categories. For each category, provide detailed reasoning explaining the score, then assign a score. Ensure the tweet maintains the same meaning as the original text."""
|
||||||
|
|
||||||
original_text: str = dspy.InputField(desc="Original input text that started the optimization")
|
original_text: str = dspy.InputField(
|
||||||
current_best_tweet: str = dspy.InputField(desc="Current best tweet version for comparison (empty for first evaluation)")
|
desc="Original input text that started the optimization"
|
||||||
|
)
|
||||||
|
current_best_tweet: str = dspy.InputField(
|
||||||
|
desc="Current best tweet version for comparison (empty for first evaluation)"
|
||||||
|
)
|
||||||
tweet_text: str = dspy.InputField(desc="Tweet text to evaluate")
|
tweet_text: str = dspy.InputField(desc="Tweet text to evaluate")
|
||||||
categories: str = dspy.InputField(desc="Comma-separated list of evaluation category descriptions")
|
categories: str = dspy.InputField(
|
||||||
|
desc="Comma-separated list of evaluation category descriptions"
|
||||||
|
)
|
||||||
evaluations: List[CategoryEvaluation] = dspy.OutputField(
|
evaluations: List[CategoryEvaluation] = dspy.OutputField(
|
||||||
desc=f"List of evaluations with category name, detailed reasoning, and score ({MIN_SCORE}-{MAX_SCORE}) for each category. Ensure the tweet conveys the same meaning as the original text."
|
desc=f"List of evaluations with category name, detailed reasoning, and score ({MIN_SCORE}-{MAX_SCORE}) for each category. Ensure the tweet conveys the same meaning as the original text."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TweetGeneratorModule(dspy.Module):
|
class TweetGeneratorModule(dspy.Module):
|
||||||
"""DSPy module for generating and improving tweets."""
|
"""DSPy module for generating and improving tweets."""
|
||||||
|
|
||||||
@@ -40,7 +55,12 @@ class TweetGeneratorModule(dspy.Module):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.generate = dspy.ChainOfThought(TweetGenerator)
|
self.generate = dspy.ChainOfThought(TweetGenerator)
|
||||||
|
|
||||||
def forward(self, input_text: str, current_tweet: str = "", previous_evaluation: Optional[EvaluationResult] = None) -> str:
|
def forward(
|
||||||
|
self,
|
||||||
|
input_text: str,
|
||||||
|
current_tweet: str = "",
|
||||||
|
previous_evaluation: Optional[EvaluationResult] = None,
|
||||||
|
) -> str:
|
||||||
"""Generate or improve a tweet."""
|
"""Generate or improve a tweet."""
|
||||||
try:
|
try:
|
||||||
# Format previous evaluation as text
|
# Format previous evaluation as text
|
||||||
@@ -49,16 +69,19 @@ class TweetGeneratorModule(dspy.Module):
|
|||||||
result = self.generate(
|
result = self.generate(
|
||||||
input_text=input_text,
|
input_text=input_text,
|
||||||
current_tweet=current_tweet,
|
current_tweet=current_tweet,
|
||||||
previous_evaluation=eval_text
|
previous_evaluation=eval_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure tweet doesn't exceed character limit
|
# Ensure tweet doesn't exceed character limit
|
||||||
tweet = truncate_tweet(result.improved_tweet, TWEET_MAX_LENGTH, TWEET_TRUNCATION_SUFFIX)
|
tweet = truncate_tweet(
|
||||||
|
result.improved_tweet, TWEET_MAX_LENGTH, TWEET_TRUNCATION_SUFFIX
|
||||||
|
)
|
||||||
|
|
||||||
return tweet
|
return tweet
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"{ERROR_GENERATION}: {str(e)}")
|
raise Exception(f"{ERROR_GENERATION}: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
class TweetEvaluatorModule(dspy.Module):
|
class TweetEvaluatorModule(dspy.Module):
|
||||||
"""DSPy module for evaluating tweets across custom categories."""
|
"""DSPy module for evaluating tweets across custom categories."""
|
||||||
|
|
||||||
@@ -66,7 +89,13 @@ class TweetEvaluatorModule(dspy.Module):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.evaluate = dspy.ChainOfThought(TweetEvaluator)
|
self.evaluate = dspy.ChainOfThought(TweetEvaluator)
|
||||||
|
|
||||||
def forward(self, tweet_text: str, categories: List[str], original_text: str = "", current_best_tweet: str = "") -> EvaluationResult:
|
def forward(
|
||||||
|
self,
|
||||||
|
tweet_text: str,
|
||||||
|
categories: List[str],
|
||||||
|
original_text: str = "",
|
||||||
|
current_best_tweet: str = "",
|
||||||
|
) -> EvaluationResult:
|
||||||
"""Evaluate a tweet across specified categories."""
|
"""Evaluate a tweet across specified categories."""
|
||||||
try:
|
try:
|
||||||
# Join categories into comma-separated string
|
# Join categories into comma-separated string
|
||||||
@@ -76,7 +105,7 @@ class TweetEvaluatorModule(dspy.Module):
|
|||||||
original_text=original_text,
|
original_text=original_text,
|
||||||
current_best_tweet=current_best_tweet,
|
current_best_tweet=current_best_tweet,
|
||||||
tweet_text=tweet_text,
|
tweet_text=tweet_text,
|
||||||
categories=categories_str
|
categories=categories_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract and validate evaluations
|
# Extract and validate evaluations
|
||||||
@@ -87,10 +116,9 @@ class TweetEvaluatorModule(dspy.Module):
|
|||||||
# Create default evaluations if mismatch
|
# Create default evaluations if mismatch
|
||||||
evaluations = [
|
evaluations = [
|
||||||
CategoryEvaluation(
|
CategoryEvaluation(
|
||||||
category=cat,
|
category=cat, reasoning=ERROR_PARSING, score=DEFAULT_SCORE
|
||||||
reasoning=ERROR_PARSING,
|
)
|
||||||
score=DEFAULT_SCORE
|
for cat in categories
|
||||||
) for cat in categories
|
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# Validate each evaluation
|
# Validate each evaluation
|
||||||
@@ -99,17 +127,27 @@ class TweetEvaluatorModule(dspy.Module):
|
|||||||
try:
|
try:
|
||||||
# Ensure score is valid
|
# Ensure score is valid
|
||||||
score = max(MIN_SCORE, min(MAX_SCORE, int(eval.score)))
|
score = max(MIN_SCORE, min(MAX_SCORE, int(eval.score)))
|
||||||
validated_evals.append(CategoryEvaluation(
|
validated_evals.append(
|
||||||
category=categories[i] if i < len(categories) else eval.category,
|
CategoryEvaluation(
|
||||||
reasoning=eval.reasoning if eval.reasoning else "No reasoning provided",
|
category=categories[i]
|
||||||
score=score
|
if i < len(categories)
|
||||||
))
|
else eval.category,
|
||||||
|
reasoning=eval.reasoning
|
||||||
|
if eval.reasoning
|
||||||
|
else "No reasoning provided",
|
||||||
|
score=score,
|
||||||
|
)
|
||||||
|
)
|
||||||
except (ValueError, TypeError, AttributeError):
|
except (ValueError, TypeError, AttributeError):
|
||||||
validated_evals.append(CategoryEvaluation(
|
validated_evals.append(
|
||||||
category=categories[i] if i < len(categories) else "Unknown",
|
CategoryEvaluation(
|
||||||
|
category=categories[i]
|
||||||
|
if i < len(categories)
|
||||||
|
else "Unknown",
|
||||||
reasoning=ERROR_VALIDATION,
|
reasoning=ERROR_VALIDATION,
|
||||||
score=DEFAULT_SCORE
|
score=DEFAULT_SCORE,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
evaluations = validated_evals
|
evaluations = validated_evals
|
||||||
|
|
||||||
# Create validated result
|
# Create validated result
|
||||||
@@ -122,7 +160,8 @@ class TweetEvaluatorModule(dspy.Module):
|
|||||||
CategoryEvaluation(
|
CategoryEvaluation(
|
||||||
category=cat,
|
category=cat,
|
||||||
reasoning=f"{ERROR_EVALUATION}: {str(e)}",
|
reasoning=f"{ERROR_EVALUATION}: {str(e)}",
|
||||||
score=DEFAULT_SCORE
|
score=DEFAULT_SCORE,
|
||||||
) for cat in categories
|
)
|
||||||
|
for cat in categories
|
||||||
]
|
]
|
||||||
return EvaluationResult(evaluations=default_evals)
|
return EvaluationResult(evaluations=default_evals)
|
||||||
|
|||||||
@@ -23,22 +23,24 @@ from .constants import (
|
|||||||
ERROR_SAVE_HISTORY,
|
ERROR_SAVE_HISTORY,
|
||||||
ERROR_LOAD_HISTORY,
|
ERROR_LOAD_HISTORY,
|
||||||
ERROR_DSPy_INIT,
|
ERROR_DSPy_INIT,
|
||||||
TWEET_MAX_LENGTH
|
TWEET_MAX_LENGTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_categories(categories: List[str]) -> None:
|
def save_categories(categories: List[str]) -> None:
|
||||||
"""Save categories to JSON file."""
|
"""Save categories to JSON file."""
|
||||||
try:
|
try:
|
||||||
with open(CATEGORIES_FILE, 'w') as f:
|
with open(CATEGORIES_FILE, "w") as f:
|
||||||
json.dump(categories, f, indent=2)
|
json.dump(categories, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{ERROR_SAVE_CATEGORIES}: {str(e)}")
|
print(f"{ERROR_SAVE_CATEGORIES}: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def load_categories() -> List[str]:
|
def load_categories() -> List[str]:
|
||||||
"""Load categories from JSON file."""
|
"""Load categories from JSON file."""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(CATEGORIES_FILE):
|
if os.path.exists(CATEGORIES_FILE):
|
||||||
with open(CATEGORIES_FILE, 'r') as f:
|
with open(CATEGORIES_FILE, "r") as f:
|
||||||
categories = json.load(f)
|
categories = json.load(f)
|
||||||
return categories if isinstance(categories, list) else []
|
return categories if isinstance(categories, list) else []
|
||||||
else:
|
else:
|
||||||
@@ -48,6 +50,7 @@ def load_categories() -> List[str]:
|
|||||||
print(f"{ERROR_LOAD_CATEGORIES}: {str(e)}")
|
print(f"{ERROR_LOAD_CATEGORIES}: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_dspy_lm(model_name: str):
|
def get_dspy_lm(model_name: str):
|
||||||
"""Get a DSPy LM instance for the specified model (cached per model)."""
|
"""Get a DSPy LM instance for the specified model (cached per model)."""
|
||||||
try:
|
try:
|
||||||
@@ -63,26 +66,26 @@ def get_dspy_lm(model_name: str):
|
|||||||
api_key=openrouter_key,
|
api_key=openrouter_key,
|
||||||
api_base=OPENROUTER_API_BASE,
|
api_base=OPENROUTER_API_BASE,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
temperature=temperature
|
temperature=temperature,
|
||||||
)
|
)
|
||||||
return lm
|
return lm
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to create LM: {str(e)}")
|
raise Exception(f"Failed to create LM: {str(e)}")
|
||||||
|
|
||||||
def initialize_dspy(model_name: str = DEFAULT_MODEL, use_cache: bool = DEFAULT_USE_CACHE) -> bool:
|
|
||||||
|
def initialize_dspy(
|
||||||
|
model_name: str = DEFAULT_MODEL, use_cache: bool = DEFAULT_USE_CACHE
|
||||||
|
) -> bool:
|
||||||
"""Initialize DSPy with OpenRouter and selected model."""
|
"""Initialize DSPy with OpenRouter and selected model."""
|
||||||
# Configure cache settings
|
# Configure cache settings
|
||||||
try:
|
try:
|
||||||
dspy.configure_cache(
|
dspy.configure_cache(enable_memory_cache=use_cache, enable_disk_cache=use_cache)
|
||||||
enable_memory_cache=use_cache,
|
|
||||||
enable_disk_cache=use_cache
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# Cache configuration might fail in some environments, continue anyway
|
# Cache configuration might fail in some environments, continue anyway
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Only configure DSPy once globally
|
# Only configure DSPy once globally
|
||||||
if not hasattr(dspy, '_replit_configured'):
|
if not hasattr(dspy, "_replit_configured"):
|
||||||
try:
|
try:
|
||||||
# Get the LM for the default model
|
# Get the LM for the default model
|
||||||
default_lm = get_dspy_lm(model_name)
|
default_lm = get_dspy_lm(model_name)
|
||||||
@@ -93,34 +96,41 @@ def initialize_dspy(model_name: str = DEFAULT_MODEL, use_cache: bool = DEFAULT_U
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def format_tweet_for_display(tweet: str) -> str:
|
def format_tweet_for_display(tweet: str) -> str:
|
||||||
"""Format tweet text for better display."""
|
"""Format tweet text for better display."""
|
||||||
return tweet.strip()
|
return tweet.strip()
|
||||||
|
|
||||||
|
|
||||||
def calculate_tweet_length(tweet: str) -> int:
|
def calculate_tweet_length(tweet: str) -> int:
|
||||||
"""Calculate tweet length."""
|
"""Calculate tweet length."""
|
||||||
return len(tweet.strip())
|
return len(tweet.strip())
|
||||||
|
|
||||||
|
|
||||||
def is_valid_tweet(tweet: str) -> bool:
|
def is_valid_tweet(tweet: str) -> bool:
|
||||||
"""Check if tweet is valid (not empty and within character limit)."""
|
"""Check if tweet is valid (not empty and within character limit)."""
|
||||||
cleaned_tweet = tweet.strip()
|
cleaned_tweet = tweet.strip()
|
||||||
return bool(cleaned_tweet) and len(cleaned_tweet) <= TWEET_MAX_LENGTH
|
return bool(cleaned_tweet) and len(cleaned_tweet) <= TWEET_MAX_LENGTH
|
||||||
|
|
||||||
|
|
||||||
def save_settings(settings: Dict[str, Any]) -> None:
|
def save_settings(settings: Dict[str, Any]) -> None:
|
||||||
"""Save settings to JSON file."""
|
"""Save settings to JSON file."""
|
||||||
try:
|
try:
|
||||||
with open(SETTINGS_FILE, 'w') as f:
|
with open(SETTINGS_FILE, "w") as f:
|
||||||
json.dump(settings, f, indent=2)
|
json.dump(settings, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{ERROR_SAVE_SETTINGS}: {str(e)}")
|
print(f"{ERROR_SAVE_SETTINGS}: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def load_settings() -> Dict[str, Any]:
|
def load_settings() -> Dict[str, Any]:
|
||||||
"""Load settings from JSON file."""
|
"""Load settings from JSON file."""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(SETTINGS_FILE):
|
if os.path.exists(SETTINGS_FILE):
|
||||||
with open(SETTINGS_FILE, 'r') as f:
|
with open(SETTINGS_FILE, "r") as f:
|
||||||
settings = json.load(f)
|
settings = json.load(f)
|
||||||
return settings if isinstance(settings, dict) else get_default_settings()
|
return (
|
||||||
|
settings if isinstance(settings, dict) else get_default_settings()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Return default settings if file doesn't exist
|
# Return default settings if file doesn't exist
|
||||||
default_settings = get_default_settings()
|
default_settings = get_default_settings()
|
||||||
@@ -130,28 +140,31 @@ def load_settings() -> Dict[str, Any]:
|
|||||||
print(f"{ERROR_LOAD_SETTINGS}: {str(e)}")
|
print(f"{ERROR_LOAD_SETTINGS}: {str(e)}")
|
||||||
return get_default_settings()
|
return get_default_settings()
|
||||||
|
|
||||||
|
|
||||||
def get_default_settings() -> Dict[str, Any]:
|
def get_default_settings() -> Dict[str, Any]:
|
||||||
"""Get default settings."""
|
"""Get default settings."""
|
||||||
return {
|
return {
|
||||||
"selected_model": DEFAULT_MODEL,
|
"selected_model": DEFAULT_MODEL,
|
||||||
"iterations": DEFAULT_ITERATIONS,
|
"iterations": DEFAULT_ITERATIONS,
|
||||||
"patience": DEFAULT_PATIENCE,
|
"patience": DEFAULT_PATIENCE,
|
||||||
"use_cache": DEFAULT_USE_CACHE
|
"use_cache": DEFAULT_USE_CACHE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def save_input_history(history: List[str]) -> None:
|
def save_input_history(history: List[str]) -> None:
|
||||||
"""Save input history to JSON file."""
|
"""Save input history to JSON file."""
|
||||||
try:
|
try:
|
||||||
with open(HISTORY_FILE, 'w') as f:
|
with open(HISTORY_FILE, "w") as f:
|
||||||
json.dump(history, f, indent=2)
|
json.dump(history, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{ERROR_SAVE_HISTORY}: {str(e)}")
|
print(f"{ERROR_SAVE_HISTORY}: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def load_input_history() -> List[str]:
|
def load_input_history() -> List[str]:
|
||||||
"""Load input history from JSON file."""
|
"""Load input history from JSON file."""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(HISTORY_FILE):
|
if os.path.exists(HISTORY_FILE):
|
||||||
with open(HISTORY_FILE, 'r') as f:
|
with open(HISTORY_FILE, "r") as f:
|
||||||
history = json.load(f)
|
history = json.load(f)
|
||||||
return history if isinstance(history, list) else []
|
return history if isinstance(history, list) else []
|
||||||
else:
|
else:
|
||||||
@@ -160,6 +173,7 @@ def load_input_history() -> List[str]:
|
|||||||
print(f"{ERROR_LOAD_HISTORY}: {str(e)}")
|
print(f"{ERROR_LOAD_HISTORY}: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def add_to_input_history(history: List[str], new_input: str) -> List[str]:
|
def add_to_input_history(history: List[str], new_input: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Add a new input to history, maintaining max size and avoiding duplicates.
|
Add a new input to history, maintaining max size and avoiding duplicates.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"AutoConfig": "agent.agent.TweetOptimizerConfig",
|
"AutoConfig": "agent.index.TweetOptimizerConfig",
|
||||||
"AutoAgent": "agent.agent.TweetOptimizerAgent"
|
"AutoAgent": "agent.index.TweetOptimizerAgent"
|
||||||
}
|
}
|
||||||
8
main.py
8
main.py
@@ -1,4 +1,5 @@
|
|||||||
from agent.agent import TweetOptimizerAgent, TweetOptimizerConfig
|
from agent.index import TweetOptimizerAgent, TweetOptimizerConfig
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# create agent with default config
|
# create agent with default config
|
||||||
@@ -16,7 +17,7 @@ def main():
|
|||||||
results = tweet_optimizer(
|
results = tweet_optimizer(
|
||||||
input_text="Anthropic added a new OSS model on HuggingFace.",
|
input_text="Anthropic added a new OSS model on HuggingFace.",
|
||||||
iterations=10, # Reduced for testing
|
iterations=10, # Reduced for testing
|
||||||
patience=8
|
patience=8,
|
||||||
)
|
)
|
||||||
print(f"Initial text: {results['initial_text']}")
|
print(f"Initial text: {results['initial_text']}")
|
||||||
print(f"Final tweet: {results['final_tweet']}")
|
print(f"Final tweet: {results['final_tweet']}")
|
||||||
@@ -33,7 +34,7 @@ def main():
|
|||||||
tweet_optimizer.push_to_hub(
|
tweet_optimizer.push_to_hub(
|
||||||
"farouk1/tweet-optimizer-v2",
|
"farouk1/tweet-optimizer-v2",
|
||||||
commit_message="Complete Migration",
|
commit_message="Complete Migration",
|
||||||
with_code=True
|
with_code=True,
|
||||||
)
|
)
|
||||||
print("Successfully pushed to hub!")
|
print("Successfully pushed to hub!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -45,5 +46,6 @@ def main():
|
|||||||
print(f"Max iterations: {config.max_iterations}")
|
print(f"Max iterations: {config.max_iterations}")
|
||||||
print(f"Patience: {config.patience}")
|
print(f"Patience: {config.patience}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user