Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,10 @@ jobs:
- name: Type check Python code
run: uv run mypy src

- name: Run ruff linter and formatter
run: |
uv run ruff check src tests examples
uv run ruff format src tests examples

- name: Check package build
run: uv build --verbose
47 changes: 12 additions & 35 deletions examples/team_recommender/src/logger_to_opentelemetry.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import logging
import sys
from typing import Any, Dict, Optional
from typing import Optional

# OpenTelemetry imports
from opentelemetry import trace
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import TracerProvider
from opentelemetry.trace.span import Span


Expand All @@ -26,6 +23,7 @@ def emit(self, record: logging.LogRecord) -> None:
"""
Convert a LogRecord to an OpenTelemetry span event.
"""
# noinspection PyBroadException
try:
# Get the current active span, if available
current_span: Span = trace.get_current_span()
Expand All @@ -51,7 +49,7 @@ def emit(self, record: logging.LogRecord) -> None:
attributes.update(record.otel_attributes)

# Format the message
message = self.format(record)
self.format(record)

# Add event to the current span
current_span.add_event(name=f"log.{record.levelname.lower()}", attributes=attributes)
Expand All @@ -73,52 +71,31 @@ def emit(self, record: logging.LogRecord) -> None:
self.handleError(record)


def configure_opentelemetry(
service_name: str, extra_resources: Optional[Dict[str, Any]] = None
) -> None:
"""Configure OpenTelemetry tracer with console exporter for demonstration."""
# Create a resource with service info
resource_attributes = {ResourceAttributes.SERVICE_NAME: service_name}

if extra_resources:
resource_attributes.update(extra_resources)

# Create and set the TracerProvider
resource = Resource.create(resource_attributes)
trace.set_tracer_provider(TracerProvider(resource=resource))

# Add console exporter for demo purposes
# In production, you'd use OTLP, Jaeger, Zipkin, etc.
processor = BatchSpanProcessor(ConsoleSpanExporter())
trace.get_tracer_provider().add_span_processor(processor)


def configure_logger_for_opentelemetry(logger_name: Optional[str] = None) -> logging.Logger:
"""Configure a logger to send events to OpenTelemetry."""
# Get logger
logger = logging.getLogger(logger_name)
ot_logger = logging.getLogger(logger_name)

# Create and add OpenTelemetry handler
otel_handler = OpenTelemetryLogHandler()
ot_handler = OpenTelemetryLogHandler()
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
otel_handler.setFormatter(formatter)
logger.addHandler(otel_handler)
ot_handler.setFormatter(formatter)
ot_logger.addHandler(ot_handler)

# You can keep console output for local development
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
ot_logger.addHandler(console_handler)

# Set level
logger.setLevel(logging.INFO)
ot_logger.setLevel(logging.INFO)

return logger
return ot_logger


# Example usage
if __name__ == "__main__":
# Configure OpenTelemetry
configure_opentelemetry("example-service")

# Configure logger
logger = configure_logger_for_opentelemetry("example.app")
Expand All @@ -136,7 +113,7 @@ def configure_logger_for_opentelemetry(logger_name: Optional[str] = None) -> log
try:
# Simulate an error
result = 1 / 0
except Exception:
logger.exception("Operation failed")
except Exception as simulated_error:
logger.exception(f"Operation failed: {simulated_error}")

logger.info("Operation completed")
25 changes: 13 additions & 12 deletions examples/team_recommender/src/retry.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
import time
import logging
import time
from functools import wraps
from typing import Any, Callable, TypeVar, Optional, Tuple, Type, Union, Dict, List
from typing import Any, Callable, Optional, Tuple, Type, TypeVar

T = TypeVar('T')
T = TypeVar("T")
logger = logging.getLogger(__name__)


def retry(
max_attempts: int = 3,
exceptions: Tuple[Type[Exception], ...] = (Exception,),
initial_delay: float = 1.0,
backoff_factor: float = 2.0,
logger_name: Optional[str] = None
logger_name: Optional[str] = None,
) -> Callable:
"""
Retry decorator with exponential backoff for handling transient errors.

Args:
max_attempts: Maximum number of attempts (including first try)
exceptions: Tuple of exception types to catch and retry
initial_delay: Initial delay between retries in seconds
backoff_factor: Multiplier for delay after each retry
logger_name: Optional logger name for custom logging

Returns:
Decorated function with retry logic
"""
local_logger = logger
if logger_name:
local_logger = logging.getLogger(logger_name)

def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
attempt = 1
current_delay = initial_delay

while True:
try:
return func(*args, **kwargs)
Expand All @@ -45,16 +46,16 @@ def wrapper(*args: Any, **kwargs: Any) -> T:
f"Failed after {max_attempts} attempts: {e.__class__.__name__}: {str(e)}"
)
raise

local_logger.warning(
f"Attempt {attempt}/{max_attempts} failed with {e.__class__.__name__}: {str(e)}. "
f"Retrying in {current_delay:.2f}s..."
)

time.sleep(current_delay)
current_delay *= backoff_factor
attempt += 1

return wrapper

return decorator
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ def test_response_shows_developer_names():
assert client is not None

system_prompt = """
You will get a description of a project, and your task is to tell me the best developers from the given list for the project
based on their skills.
You will get a description of a project, and your task is
to tell me the best developers from the given list for the project based on their skills.
Today's date is April 15th, 2025.
Pick only developers who are available after the project start date. Pick people with higher skill levels first.
Pick only developers who are available after the project start date.
Pick people with higher skill levels first.

Here is the skills data:
Sam Thomas - Swift, Objective-C
Drew Anderson - Swift, on vacation June 1st - June 10th
Joe Smith - Android
Robert Sanders - React Native
"""
Sam Thomas - Swift, Objective-C
Drew Anderson - Swift, on vacation June 1st - June 10th
Joe Smith - Android
Robert Sanders - React Native
"""

project_description = """
This is a mobile iOS project for a telecom company. The project starts June 3rd.
Expand All @@ -35,19 +36,23 @@ def test_response_shows_developer_names():
# For the iOS Native project starting on June 3rd, the best developers based on the given list would be:
#
# 1. Sam Thomas - Specializes in Swift and Objective-C, and is available for the project.
# 2. Drew Anderson - Specializes in Swift but will be on vacation from June 1st to June 10th, so they are not available when the project starts.
# 2. Drew Anderson - Specializes in Swift but will be on vacation from June 1st to June 10th,
# so they are not available when the project starts.
#
# Therefore, Sam Thomas is the most suitable developer for this project.
assert "Sam Thomas" in response
assert "Drew Anderson" in response, "Surprisingly Drew Anderson is on vacation but still in the response"
assert "Drew Anderson" in response, (
"Surprisingly Drew Anderson is on vacation but still in the response"
)


def test_llm_will_hallucinate_given_no_data():
client = OpenAI()
assert client is not None

system_prompt = """
You will get a description of a project, and your task is to tell me the best developers from the given list for the project
based on their skills.
You will get a description of a project, and your task is to tell me the best developers
from the given list for the project based on their skills.
Today's date is April 15th, 2025.
Pick only developers who are available after the project start date. Pick people with higher skill levels first.

Expand Down Expand Up @@ -83,7 +88,8 @@ def test_llm_will_hallucinate_given_no_data():
# - Skills: iOS Native, Mobile UI Design
# - Availability: Available starting May 20th
#
# Based on the project requirements and availability, the best developer for this mobile iOS project for the telecom company would be:
# Based on the project requirements and availability, the best developer for this mobile iOS project
# for the telecom company would be:
#
# 1. Sarah Johnson
# - Skills: iOS Native, Mobile Development
Expand All @@ -92,6 +98,10 @@ def test_llm_will_hallucinate_given_no_data():
# 2. Jamie Smith
# - Skills: iOS Native, Mobile UI Design
# - Availability: Available starting May 20th
assert "Sam Thomas" not in response, "LLM obviously could not get our expected developer and will hallucinate"
assert "Sam Thomas" not in response, (
"LLM obviously could not get our expected developer and will hallucinate"
)
assert "Drew Anderson" not in response, "Response will contain made up names"
assert len(response.split('\n')) > 5, "response contains list of made up developers in multiple lines"
assert len(response.split("\n")) > 5, (
"response contains list of made up developers in multiple lines"
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ def test_allocations():
acceptable_people = ["Sam Thomas", "Drew Anderson", "Alex Wilson", "Alex Johnson"]

system_prompt = f"""
You will get a description of a project, and your task is to tell me the best developers from the given list for the project
based on their skills.
You will get a description of a project, and your task is
to tell me the best developers from the given list for the project based on their skills.
Today's date is April 15th, 2025.
Pick only developers who are available after the project start date. Pick people with higher skill levels first.
respond in json with this structure:
Pick only developers who are available after the project start date.
Pick people with higher skill levels first.
Respond in json with this structure:
{example_output}

Here is the skills data:
"""

system_prompt = system_prompt + str(skills_data)

project_description = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ def test_allocations():
skills_data = load_json_fixture("skills.json")
example_output = load_json_fixture("example_output.json")
system_prompt = f"""
You will get a description of a project, and your task is to tell me the best developers from the given list for the project
based on their skills.
You will get a description of a project, and your task is
to tell me the best developers from the given list for the project based on their skills.
Today's date is April 15th, 2025.
Pick only developers who are available after the project start date. Pick people with higher skill levels first.
respond in json with this structure:
Pick only developers who are available after the project start date.
Pick people with higher skill levels first.
Respond in json with this structure:
{example_output}

Here is the skills data:
"""

system_prompt = system_prompt + str(skills_data)

project_description = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@ def get_all_developer_names(skills_data) -> set[str]:
}


def get_developer_names_from_response(response) -> set[str]:
def get_developer_names_from_response(response: dict[str, dict[str, str]]) -> set[str]:
return {developer["name"] for developer in response["developers"]}


def test_allocations():
skills_data = load_json_fixture("skills.json")
example_output = load_json_fixture("example_output.json")
system_prompt = f"""
You will get a description of a project, and your task is to tell me the best developers from the given list for the project
based on their skills.
You will get a description of a project, and your task is
to tell me the best developers from the given list for the project based on their skills.
Today's date is April 15th, 2025.
Pick only developers who are available after the project start date. Pick people with higher skill levels first.
respond in json with this structure:
Pick only developers who are available after the project start date.
Pick people with higher skill levels first.
Respond in json with this structure:
{example_output}

Here is the skills data:
Expand Down Expand Up @@ -71,11 +72,11 @@ def run_allocation_test(reporter, skills_data) -> bool:
response_format={"type": "json_object"},
)
response = completion.choices[0].message.content
result = False
json_load_success = False
not_empty_response = True
no_developer_name_is_hallucinated = True
developer_is_appropriate = True
json_object = {}
try:
json_object = json.loads(response)
json_load_success = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ def test_allocations():
skills_data = load_json_fixture("skills.json")
example_output = load_json_fixture("example_output.json")
system_prompt = f"""
You will get a description of a project, and your task is to tell me the best developers from the given list for the project
based on their skills.
You will get a description of a project, and your task is
to tell me the best developers from the given list for the project based on their skills.
Today's date is April 15th, 2025.
Pick only developers who are available after the project start date. Pick people with higher skill levels first.
respond in json with this structure:
Pick only developers who are available after the project start date.
Pick people with higher skill levels first.
Respond in json with this structure:
{example_output}

Here is the skills data:
"""

system_prompt = system_prompt + str(skills_data)

project_description = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ def test_fast_with_n_generations():
skills_data = load_json_fixture("skills.json")
example_output = load_json_fixture("example_output.json")
system_prompt = f"""
You will get a description of a project, and your task is to tell me the best developers from the given list for the project
based on their skills.
You will get a description of a project, and your task is
to tell me the best developers from the given list for the project based on their skills.
Today's date is April 15th, 2025.
Pick only developers who are available after the project start date. Pick people with higher skill levels first.
respond in json with this structure:
Pick only developers who are available after the project start date.
Pick people with higher skill levels first.
Respond in json with this structure:
{example_output}

Here is the skills data:
"""

system_prompt = system_prompt + str(skills_data)

project_description = """
Expand Down Expand Up @@ -77,8 +79,8 @@ def test_fast_with_n_generations():
output_dir=ROOT_DIR,
)
test_runner = Runner(
lambda reporter: run_allocation_test(
reporter, skills_data=skills_data, response=response
lambda reporter, content=response: run_allocation_test(
reporter, skills_data=skills_data, response=content
),
reporter=test_reporter,
)
Expand Down
Loading