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
26 changes: 15 additions & 11 deletions codeplain_REST_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
from typing import Optional

import requests
from requests.exceptions import ConnectionError, RequestException, Timeout

import plain2code_exceptions
from plain2code_state import RunState

MAX_RETRIES = 4
RETRY_DELAY = 3

# TODO: Handle connection errors
RETRY_ERROR_CODES = [
"LLMInternalError",
]


class CodeplainAPI:

Expand All @@ -34,6 +38,7 @@ def post_request(self, endpoint_url, headers, payload, run_state: Optional[RunSt
self._extend_payload_with_run_state(payload, run_state)

retry_delay = RETRY_DELAY
response_json = None
for attempt in range(MAX_RETRIES + 1):
try:
response = requests.post(endpoint_url, headers=headers, json=payload)
Expand Down Expand Up @@ -65,27 +70,26 @@ def post_request(self, endpoint_url, headers, payload, run_state: Optional[RunSt
if response_json["error_code"] == "PlainSyntaxError":
raise plain2code_exceptions.PlainSyntaxError(response_json["message"])

if response_json["error_code"] == "NoRenderFound":
raise plain2code_exceptions.NoRenderFound(response_json["message"])

if response_json["error_code"] == "MultipleRendersFound":
raise plain2code_exceptions.MultipleRendersFound(response_json["message"])
if response_json["error_code"] == "InternalServerError":
raise plain2code_exceptions.InternalServerError(response_json["message"])

response.raise_for_status()
return response_json

except (ConnectionError, Timeout, RequestException) as e:
except Exception as e:
if response_json is not None and "error_code" in response_json:
if response_json["error_code"] not in RETRY_ERROR_CODES:
raise e

if attempt < MAX_RETRIES:
self.console.info(f"Connection error on attempt {attempt + 1}/{MAX_RETRIES + 1}: {e}")
self.console.info(f"Error on attempt {attempt + 1}/{MAX_RETRIES + 1}: {e}")
self.console.info(f"Retrying in {retry_delay} seconds...")
time.sleep(retry_delay)
# Exponential backoff
retry_delay *= 2
else:
self.console.error(f"Max retries ({MAX_RETRIES}) exceeded. Last error: {e}")
raise RequestException(
f"Connection error: Unable to reach the Codeplain API at {self.api_url}. Please try again or contact support."
)
raise e

def render_functional_requirement(
self,
Expand Down
44 changes: 40 additions & 4 deletions plain2code.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@
from module_renderer import ModuleRenderer
from plain2code_arguments import parse_arguments
from plain2code_console import console
from plain2code_exceptions import InvalidFridArgument, MissingAPIKey, PlainSyntaxError
from plain2code_exceptions import (
ConflictingRequirements,
CreditBalanceTooLow,
InternalServerError,
InvalidFridArgument,
LLMInternalError,
MissingAPIKey,
MissingResource,
PlainSyntaxError,
UnexpectedState,
)
from plain2code_logger import (
CrashLogHandler,
IndentedFormatter,
Expand Down Expand Up @@ -89,6 +99,7 @@ def setup_logging(
log_to_file: bool,
log_file_name: str,
plain_file_path: Optional[str],
render_id: str,
):
# Set default level to INFO for everything not explicitly configured
logging.getLogger().setLevel(logging.INFO)
Expand Down Expand Up @@ -148,6 +159,8 @@ def setup_logging(
crash_handler.setFormatter(formatter)
root_logger.addHandler(crash_handler)

root_logger.info(f"Render ID: {render_id}") # Ensure render ID is logged in to codeplain.log file


def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # noqa: C901
# Check system requirements before proceeding
Expand Down Expand Up @@ -202,25 +215,26 @@ def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # no
return


def main():
def main(): # noqa: C901
args = parse_arguments()

event_bus = EventBus()

setup_logging(args, event_bus, args.log_to_file, args.log_file_name, args.filename)

if not args.api:
args.api = "https://api.codeplain.ai"

run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)

setup_logging(args, event_bus, args.log_to_file, args.log_file_name, args.filename, run_state.render_id)

try:
# Validate API key is present
if not args.api_key:
raise MissingAPIKey(
"API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument."
)

console.debug(f"Render ID: {run_state.render_id}") # Ensure render ID is logged to the console
render(args, run_state, codeplain_api, event_bus)
except InvalidFridArgument as e:
console.error(f"Invalid FRID argument: {str(e)}.\n")
Expand Down Expand Up @@ -249,6 +263,28 @@ def main():
dump_crash_logs(args)
except MissingAPIKey as e:
console.error(f"Missing API key: {str(e)}\n")
except (InternalServerError, UnexpectedState) as e:
console.error(
f"Internal server error: {str(e)}.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file."
)
console.debug(f"Render ID: {run_state.render_id}")
dump_crash_logs(args)
except ConflictingRequirements as e:
console.error(f"Conflicting requirements: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
dump_crash_logs(args)
except CreditBalanceTooLow as e:
console.error(f"Credit balance too low: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
dump_crash_logs(args)
except LLMInternalError as e:
console.error(f"LLM internal error: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
dump_crash_logs(args)
except MissingResource as e:
console.error(f"Missing resource: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
dump_crash_logs(args)
except Exception as e:
console.error(f"Error rendering plain code: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
Expand Down
12 changes: 4 additions & 8 deletions plain2code_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ class PlainSyntaxError(Exception):
pass


class NoRenderFound(Exception):
pass


class MultipleRendersFound(Exception):
pass


class UnexpectedState(Exception):
pass

Expand All @@ -57,3 +49,7 @@ class InvalidLiquidVariableName(Exception):

class ModuleDoesNotExistError(Exception):
pass


class InternalServerError(Exception):
pass
8 changes: 5 additions & 3 deletions plain2code_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import Optional

from event_bus import EventBus
from plain2code_console import console
from plain2code_events import LogMessageEmitted


Expand Down Expand Up @@ -63,6 +62,10 @@ def emit(self, record):
self.event_bus.publish(event)
else:
self._buffer.append(event)
except RuntimeError:
# We're going to get this crash after the TUI app is closed (forcefully).
# NOTE: This should be more thought out.
pass
except Exception:
self.handleError(record)

Expand Down Expand Up @@ -123,5 +126,4 @@ def dump_crash_logs(args, formatter=None):
if crash_handler and args.filename:
log_file_path = get_log_file_path(args.filename, args.log_file_name)

if crash_handler.dump_to_file(log_file_path, formatter):
console.error(f"\nLogs have been dumped to {log_file_path}")
crash_handler.dump_to_file(log_file_path, formatter)