Skip to content

Enhance logging and stdin/stdout interface #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ docs/recordings/*
etc/
example_projects/
lib/
logs/
share/
src/ansari_backend.egg-info/*
tmp/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 91 additions & 13 deletions src/ansari/ansari_logger.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,76 @@
# This file provides a standard Python logging instance for the caller file (e.g., main_api.py, etc.).

import logging
import sys
import os
import re
import time

from rich.console import Console
from rich.logging import RichHandler
from rich.traceback import install as install_rich_traceback

from ansari.config import get_settings


# Install rich traceback handler globally
install_rich_traceback(
show_locals=True,
max_frames=10,
suppress=[],
width=None,
word_wrap=True,
)


def create_file_handler(name: str, logging_level: str) -> logging.FileHandler:
"""Creates and configures a file handler for logging.

Args:
name (str): The name of the module for the log file.
logging_level (str): The logging level to set for the handler.

Returns:
logging.FileHandler: Configured file handler instance.
"""
# Ensure logs directory exists
log_dir = os.path.join(os.getcwd(), "logs")
os.makedirs(log_dir, exist_ok=True)

log_file = os.path.join(log_dir, f"{name}.log")
file_handler = logging.FileHandler(
filename=log_file,
mode="a", # Append mode
encoding="utf-8", # Use UTF-8 encoding to support Unicode characters
)
file_handler.setLevel(logging_level)

# Custom formatter for files with forward slashes and function name in square brackets
class VSCodePathFormatter(logging.Formatter):
# ANSI color code regex pattern
ANSI_ESCAPE_PATTERN = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")

def format(self, record):
# Format path with forward slashes and function name in square brackets
path_format = f"{record.name.replace('.','/')}:{record.lineno} [{record.funcName}()]"

# Format time without milliseconds
# Override the default formatTime to remove milliseconds
created = self.converter(record.created)
time_format = time.strftime("%Y-%m-%d %H:%M:%S", created)

# Get the message and strip any ANSI color codes
message = record.getMessage()
clean_message = self.ANSI_ESCAPE_PATTERN.sub("", message)

# Combine everything
return f"{time_format} | {record.levelname} | {path_format} | {clean_message}"

# Use the custom formatter for files
file_formatter = VSCodePathFormatter() # No datefmt needed as we're formatting time manually
file_handler.setFormatter(file_formatter)
return file_handler


def get_logger(name: str) -> logging.Logger:
"""Creates and returns a logger instance for the specified module.

Expand All @@ -17,6 +82,14 @@ def get_logger(name: str) -> logging.Logger:
"""
logging_level = get_settings().LOGGING_LEVEL.upper()

# Create a Rich console for logging
console = Console(
highlight=True, # Syntax highlighting
markup=True, # Enable Rich markup
log_path=False, # Don't write to a log file directly - we'll handle that separately
log_time_format="%Y-%m-%d %H:%M:%S",
)

# Create a logger
logger = logging.getLogger(name)

Expand All @@ -27,20 +100,25 @@ def get_logger(name: str) -> logging.Logger:
# Set the logging level
logger.setLevel(logging_level)

# Create console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging_level)

# Create formatter
formatter = logging.Formatter(
"%(asctime)s | %(levelname)s | %(name)s:%(funcName)s:%(lineno)d | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
# Create Rich handler with VS Code compatible formatting in DEV_MODE
rich_handler = RichHandler(
console=console,
enable_link_path=get_settings().DEV_MODE, # Enable VS Code clickable links in DEV_MODE
markup=True,
rich_tracebacks=True,
tracebacks_show_locals=True,
show_time=True,
show_level=True,
show_path=True,
)
rich_handler.setLevel(logging_level)

# Add formatter to handler
console_handler.setFormatter(formatter)
# Add the Rich handler to the logger
logger.addHandler(rich_handler)

# Add handler to logger
logger.addHandler(console_handler)
# Add file handler if DEV_MODE is enabled
if get_settings().DEV_MODE:
file_handler = create_file_handler(name, logging_level)
logger.addHandler(file_handler)

return logger
26 changes: 21 additions & 5 deletions src/ansari/app/main_stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,27 @@ def main(
input: Optional[str] = typer.Option(
None, "--input", "-i", help="Input to send to the agent. If not provided, starts interactive mode."
),
stream: bool = typer.Option(
False, "--stream", "-s", help="Stream the output word by word. If False, prints the complete answer at once."
),
):
"""
Run the Ansari agent. If input is provided, process it and exit.
If no input is provided, start interactive mode.
"""
# Convert log level string to logging constant
# Get the root logger and set its level to match the CLI argument
# Note: We don't use logging.basicConfig() to avoid duplicate logs
# Our custom get_logger() function has already configured the loggers
numeric_level = getattr(logging, log_level.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError(f"Invalid log level: {log_level}")

logging.basicConfig(level=numeric_level)
# Update the logger's level
logging.getLogger().setLevel(numeric_level)
# Also update our module's logger level
logger.setLevel(numeric_level)

settings = get_settings()

if agent == "AnsariClaude":
Expand All @@ -53,13 +63,19 @@ def main(
result = agent_instance.process_input(input)
# Handle the result which could be either a generator or other iterable
if result:
for word in result:
if word is not None:
print("Model response:")
if stream:
# Stream output word by word
for word in result:
print(word, end="", flush=True)
print()
print()
else:
# Collect the entire response and print at once
complete_response = "".join([word for word in result if word is not None])
print(complete_response)
else:
# No input provided, start interactive mode
presenter = StdioPresenter(agent_instance, skip_greeting=True)
presenter = StdioPresenter(agent_instance, skip_greeting=True, stream=stream)
presenter.present()


Expand Down
14 changes: 11 additions & 3 deletions src/ansari/presenters/stdio_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@


class StdioPresenter:
def __init__(self, agent: Ansari, skip_greeting=False):
def __init__(self, agent: Ansari, skip_greeting=False, stream=False):
self.agent = agent
self.skip_greeting = skip_greeting
self.stream = stream

def present(self):
if not self.skip_greeting:
Expand All @@ -18,10 +19,17 @@ def present(self):
result = self.agent.process_input(inp)
# Handle the result which could be either a generator or other iterable
if result:
for word in result:
if word is not None:
print("Model response:")
if self.stream:
# Stream output word by word
for word in result:
sys.stdout.write(word)
sys.stdout.flush()
else:
# Collect the entire response and output at once
complete_response = "".join([word for word in result])
sys.stdout.write(complete_response)
sys.stdout.flush()
sys.stdout.write("\n> ")
sys.stdout.flush()
inp = sys.stdin.readline()