Katkat

Refactor a Chatbot into Clean Object-Oriented Components

OpenAISoftware Engineer · Technical Screen

Last updated

Tests object-oriented design and refactoring skills by decoupling a monolithic chatbot script into modular, testable, and extensible components adhering to SOLID principles.

Refactor a Monolithic Chatbot

You are handed a prototype chatbot implementation that has grown organically into a single monolithic class. The current implementation mixes conversation memory management, prompt formatting, context window pruning, tool calling, and mock model invocation in a single method.

Your task is to refactor this architecture into clean, modular, object-oriented components following SOLID design principles.

Legacy Code Issues

The legacy system has the following liabilities:

  • Tightly coupled to a single hardcoded mock LLM API.
  • Hardcoded string matching for tools instead of a dynamic registry.
  • Memory truncation is mixed directly into the API request generation.
  • No clean way to inject guards/moderation or swap out storage.

Requirements

Design and implement a clean component hierarchy with the following responsibilities:

  1. Message & Memory/Context Management:

    • Model messages as structured entities (role, content, optional tool_calls/tool_call_id).
    • Implement a conversation memory store that retains system instructions while truncating older dialogue when a message budget (e.g., max_messages) is exceeded.
  2. Tool / Plugin Abstraction:

    • Define a base interface for tools with a name, description, and an execute(**kwargs) -> str method.
    • Implement a ToolRegistry that allows dynamic registration and execution of tools based on LLM output.
  3. LLM Client Interface:

    • Define an abstract interface LLMClient with a method generate(messages: list[Message], tools: list[dict]) -> LLMResponse.
    • Implement a MockLLMClient for testing.
  4. Orchestrator (ChatEngine):

    • Coordinates interaction between user input, memory, tool invocation, and the LLM client.
    • Handles tool-call loops: if the model returns a tool call, execute it, append the tool response to memory, and query the model again for the final response.

Class Interface Example

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional

class Message:
    def __init__(self, role: str, content: str, tool_call: Optional[Dict[str, Any]] = None):
        ...

class Tool(ABC):
    name: str
    description: str
    @abstractmethod
    def execute(self, **kwargs) -> str:
        ...

class ConversationMemory:
    def __init__(self, max_messages: int = 10):
        ...
    def add_message(self, message: Message) -> None:
        ...
    def get_context(self) -> List[Message]:
        ...

class ChatEngine:
    def __init__(self, llm_client: "LLMClient", memory: ConversationMemory, tools: Optional["ToolRegistry"] = None):
        ...
    def process_user_input(self, text: str) -> str:
        ...

Edge Cases to Consider

  • Preservation of the system message regardless of how aggressively dialogue is truncated.
  • Handling unknown tool names gracefully without crashing the loop.
  • Preventing infinite tool-execution loops (e.g., enforce a max_tool_iterations limit).

Hints

Hint 1

Separate stateful components (like conversation history) from stateless functional components (like tool execution and API clients).

Hint 2

Ensure the system prompt is treated as invariant during memory truncation while older user/assistant message pairs are pruned first.

Hint 3

Use dependency injection in the main orchestrator so that different LLM providers or storage strategies can be supplied without changing the engine logic.

Follow-up questions

  • How would you adapt your design to support streaming responses and progressive tool execution?
  • How would you integrate a token-based context window manager rather than a simple message-count cutoff?
  • How would you support concurrent conversations across multiple users while maintaining thread safety?