import sys from argparse import Namespace from asyncio import run from core.agents.orchestrator import Orchestrator from core.cli.helpers import delete_project, init, list_projects, list_projects_json, load_project, show_config from core.db.session import SessionManager from core.db.v0importer import LegacyDatabaseImporter from core.llm.base import APIError from core.log import get_logger from core.state.state_manager import StateManager from core.telemetry import telemetry from core.ui.base import UIBase log = get_logger(__name__) async def run_project(sm: StateManager, ui: UIBase) -> bool: """ Work on the project. Starts the orchestrator agent with the newly loaded/created project and runs it until the orchestrator decides to exit. :param sm: State manager. :param ui: User interface. :return: True if the orchestrator exited successfully, False otherwise. """ telemetry.start() telemetry.set("app_id", str(sm.project.id)) telemetry.set("initial_prompt", sm.current_state.specification.description) orca = Orchestrator(sm, ui) success = False try: success = await orca.run() except KeyboardInterrupt: log.info("Interrupted by user") telemetry.set("end_result", "interrupt") await sm.rollback() except APIError as err: log.warning(f"LLM API error occurred: {err.message}") await ui.send_message(f"LLM API error occurred: {err.message}") await ui.send_message("Stopping Pythagora due to previous error.") telemetry.set("end_result", "failure:api-error") await sm.rollback() except Exception as err: telemetry.record_crash(err) await sm.rollback() log.error(f"Uncaught exception: {err}", exc_info=True) await ui.send_message(f"Unrecoverable error occurred: {err}") if success: telemetry.set("end_result", "success:exit") else: # We assume unsuccessful exit (but not an exception) is a result # of an API error that the user didn't retry. telemetry.set("end_result", "failure:api-error") await telemetry.send() return success async def start_new_project(sm: StateManager, ui: UIBase) -> bool: """ Start a new project. :param sm: State manager. :param ui: User interface. :return: True if the project was created successfully, False otherwise. """ user_input = await ui.ask_question("What is the name of the project", allow_empty=False) if user_input.cancelled: return False project_state = await sm.create_project(user_input.text) return project_state is not None async def async_main( ui: UIBase, db: SessionManager, args: Namespace, ) -> bool: """ Main application coroutine. :param ui: User interface. :param db: Database session manager. :param args: Command-line arguments. :return: True if the application ran successfully, False otherwise. """ if args.list: await list_projects(db) return True elif args.list_json: await list_projects_json(db) return True if args.show_config: show_config() return True elif args.import_v0: importer = LegacyDatabaseImporter(db, args.import_v0) await importer.import_database() return True telemetry.set("user_contact", args.email) if args.extension_version: telemetry.set("is_extension", True) telemetry.set("extension_version", args.extension_version) sm = StateManager(db, ui) ui_started = await ui.start() if not ui_started: return False if args.project or args.branch or args.step: telemetry.set("is_continuation", True) # FIXME: we should send the project stage and other runtime info to the UI success = await load_project(sm, args.project, args.branch, args.step) if not success: return False elif args.delete: success = await delete_project(sm, args.delete) return success else: success = await start_new_project(sm, ui) if not success: return False return await run_project(sm, ui) def run_pythagora(): ui, db, args = init() if not ui or not db: return -1 success = run(async_main(ui, db, args)) return 0 if success else -1 if __name__ == "__main__": sys.exit(run_pythagora())