Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -517,7 +517,8 @@ def orchestrate_development(client, project_state: dict, config, oauth_token_tok
|
|
517 |
project_state['files'] = {}
|
518 |
project_state['iframe_ok'] = False
|
519 |
project_state['log_error_state'] = 'None'
|
520 |
-
|
|
|
521 |
return # Exit after initialization, next call will run the first task
|
522 |
|
523 |
if project_state['status'] != 'In Progress':
|
@@ -535,7 +536,7 @@ def orchestrate_development(client, project_state: dict, config, oauth_token_tok
|
|
535 |
if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != task_message.strip():
|
536 |
project_state['chat_history'].append({"role": "assistant", "content": task_message})
|
537 |
|
538 |
-
|
539 |
|
540 |
if current_task == 'START':
|
541 |
# This state should only be hit once for initialization, handled above.
|
@@ -599,7 +600,7 @@ This is an auto-generated HF Space.
|
|
599 |
project_state['current_task'] = 'PUSHING'
|
600 |
# Clear previous logs and feedback before pushing a new version
|
601 |
project_state['logs'] = {'build': '', 'run': ''}
|
602 |
-
project_state['feedback'] = ''
|
603 |
project_state['iframe_ok'] = False
|
604 |
project_state['log_error_state'] = 'None' # Reset log error state
|
605 |
# Do NOT reset attempt count here, it's for total cycles
|
@@ -615,7 +616,6 @@ This is an auto-generated HF Space.
|
|
615 |
project_state['status_message'] = "ERROR: Cannot push without Hugging Face token."
|
616 |
project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
|
617 |
project_state['current_task'] = 'FINISHED'
|
618 |
-
step_successful = False
|
619 |
print(project_state['status_message'])
|
620 |
return # Exit early on critical error
|
621 |
|
@@ -642,6 +642,7 @@ This is an auto-generated HF Space.
|
|
642 |
|
643 |
# Use a temporary directory to avoid cluttering the working directory
|
644 |
import tempfile
|
|
|
645 |
with tempfile.TemporaryDirectory() as tmpdir:
|
646 |
temp_file_paths = []
|
647 |
for fn, content in files_to_push.items():
|
@@ -654,39 +655,45 @@ This is an auto-generated HF Space.
|
|
654 |
temp_file_paths.append(full_path)
|
655 |
except Exception as e:
|
656 |
print(f"Error writing temporary file {fn}: {e}")
|
657 |
-
# Decide how to handle: skip file? Fail push? For now,
|
|
|
658 |
project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError writing file {fn} for push: {e}"
|
|
|
659 |
|
660 |
|
661 |
-
if not temp_file_paths:
|
662 |
print("No valid files prepared for push.")
|
663 |
-
project_state['status_message'] = "ERROR: No files were prepared for push after Code-Gen. Project Failed
|
664 |
project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
|
665 |
project_state['status'] = 'Failed'
|
666 |
project_state['current_task'] = 'FINISHED'
|
667 |
step_successful = False
|
668 |
-
return
|
669 |
-
|
670 |
-
|
671 |
-
# Upload files one by one
|
672 |
-
|
673 |
-
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
-
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
-
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
|
689 |
-
|
|
|
|
|
|
|
|
|
690 |
|
691 |
if step_successful:
|
692 |
project_state['status_message'] = f"Pushed code to HF Space **{repo_id}**. Build triggered. Waiting for build and logs..."
|
@@ -696,11 +703,13 @@ This is an auto-generated HF Space.
|
|
696 |
# Logs, iframe_ok, log_error_state were reset before push, will be updated by poller
|
697 |
|
698 |
else:
|
699 |
-
# If any upload failed, the push step failed
|
700 |
project_state['status'] = 'Failed'
|
701 |
project_state['current_task'] = 'FINISHED'
|
702 |
-
project_state['status_message'] = project_state.get('status_message', f"ERROR: One or more files failed to upload to HF Space {repo_id}. See feedback.")
|
703 |
-
|
|
|
|
|
704 |
print(project_state['status_message'])
|
705 |
|
706 |
|
@@ -747,16 +756,16 @@ This is an auto-generated HF Space.
|
|
747 |
project_state['status_message'] = "Iframe check passed. Analyzing build/run logs."
|
748 |
|
749 |
# Check for explicit log fetching errors
|
750 |
-
elif log_error_state
|
751 |
print(f"Orchestrator: Detected log fetching error state: {log_error_state}. Moving to Debugging.")
|
752 |
ready_to_debug = True
|
753 |
project_state['status_message'] = f"Log fetching encountered an error ({log_error_state}). Moving to Debugging."
|
754 |
|
755 |
# Check for significant logs that indicate build/run started/failed
|
756 |
# Avoid triggering debug if it's just the 'No logs found yet' message
|
757 |
-
elif not (build_logs.
|
758 |
# Check for actual log content or error indicators within logs
|
759 |
-
if len(build_logs) > 50 or len(run_logs) > 10 or "ERROR" in build_logs.upper() or "FATAL" in build_logs.upper() or "ERROR" in run_logs.upper() or "FATAL" in run_logs.upper():
|
760 |
print("Orchestrator: Detected significant logs. Moving to Debugging.")
|
761 |
ready_to_debug = True
|
762 |
project_state['status_message'] = "Significant logs detected. Analyzing build/run logs."
|
@@ -765,20 +774,17 @@ This is an auto-generated HF Space.
|
|
765 |
# Timeout check - Use attempt count as a global timeout for the entire process,
|
766 |
# but also use it to limit time spent *just* in LOGGING if no progress is detected.
|
767 |
if project_state['attempt_count'] >= 6: # Use attempt count as a global timeout trigger
|
768 |
-
print("Orchestrator: Max attempts reached in LOGGING. Forcing move to Debugging.")
|
769 |
ready_to_debug = True # Force debug to get final feedback before failing
|
770 |
|
771 |
|
772 |
if ready_to_debug:
|
773 |
project_state['current_task'] = 'DEBUGGING'
|
774 |
# Add a small delay before debugging starts, giving UI time to update logs
|
775 |
-
time.sleep(2) #
|
776 |
-
|
777 |
else:
|
778 |
# If not ready to debug, stay in LOGGING. The orchestrator loop will naturally pause between calls.
|
779 |
-
#
|
780 |
-
# The attempt count is more for the overall process, but we'll use it to track cycles spent here.
|
781 |
-
# The attempt count is incremented globally at the end of the loop if not finished/failed.
|
782 |
pass # Stay in LOGGING, loop will repeat
|
783 |
|
784 |
|
@@ -817,9 +823,9 @@ This is an auto-generated HF Space.
|
|
817 |
is_failed = True
|
818 |
project_state['status_message'] = f"Max attempts ({project_state['attempt_count']+1}/7) reached. Project failed."
|
819 |
print(f"Debug Analysis: Failed due to max attempts ({project_state['attempt_count']+1}).")
|
820 |
-
elif log_error_state in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Request Error', 'Unexpected Error']: # Critical log fetching errors
|
821 |
is_failed = True
|
822 |
-
project_state['status_message'] = f"Project failed due to critical log fetching error: {log_error_state}"
|
823 |
print(f"Debug Analysis: Failed due to critical log error: {log_error_state}.")
|
824 |
elif ("ERROR" in feedback.upper() or error_types != "none") and project_state['attempt_count'] >= 4:
|
825 |
# If debugger finds errors or classified errors exist after several attempts (e.g., attempts 5, 6, 7)
|
@@ -857,7 +863,7 @@ This is an auto-generated HF Space.
|
|
857 |
|
858 |
else:
|
859 |
# Should not happen - unknown state
|
860 |
-
step_successful = False # Force failure path
|
861 |
project_state['status'] = 'Failed'
|
862 |
project_state['status_message'] = f"ERROR: Orchestrator entered an unknown task state: {current_task}"
|
863 |
project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
|
@@ -873,6 +879,7 @@ This is an auto-generated HF Space.
|
|
873 |
# time.sleep(1) # Adding a 1-second pause between steps
|
874 |
|
875 |
# Final outcome message when status changes to Complete or Failed
|
|
|
876 |
if project_state['status'] != 'In Progress' and not any(msg.get('content', '').strip().startswith("**Project Outcome:") for msg in project_state['chat_history']):
|
877 |
final_outcome_message = f"**Project Outcome:** {project_state['status']} - {project_state.get('status_message', 'No specific outcome message.')}"
|
878 |
project_state['chat_history'].append({"role": "assistant", "content": final_outcome_message})
|
@@ -887,7 +894,7 @@ This is an auto-generated HF Space.
|
|
887 |
project_state['chat_history'].append({"role": "assistant", "content": failure_message})
|
888 |
|
889 |
|
890 |
-
#
|
891 |
# It updates the project_state dictionary, which is a gr.State,
|
892 |
# and the UI outputs are updated by the poller (_update_logs_and_preview)
|
893 |
# or by the main handle_user_message function after calling orchestrate_development.
|
@@ -916,159 +923,147 @@ def _update_logs_and_preview(profile_token_state: tuple[gr.OAuthProfile | None,
|
|
916 |
rid = f"{profile.username}/{clean}"
|
917 |
url = f"https://huggingface.co/spaces/{profile.username}/{clean}"
|
918 |
|
919 |
-
#
|
920 |
-
# This shouldn't happen in the intended flow, but defensively check.
|
921 |
if current_project_state is None:
|
|
|
922 |
current_project_state = {
|
923 |
'repo_id': rid, # Set repo_id based on current UI inputs
|
924 |
'iframe_url': url, # Set iframe_url based on current UI inputs
|
925 |
-
'logs': {'build': '
|
926 |
'iframe_ok': False,
|
927 |
-
'log_error_state': '
|
928 |
-
'chat_history': [],
|
929 |
'status': 'Not Started',
|
930 |
-
'status_message': 'Enter requirements to start.',
|
931 |
'attempt_count': 0,
|
932 |
'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
|
933 |
}
|
|
|
|
|
|
|
|
|
934 |
elif current_project_state.get('repo_id') != rid:
|
935 |
# If the UI Space name changes while a project is in progress for a *different* space,
|
936 |
# reset the project state to focus on the new space name.
|
937 |
-
# Alternatively, could just fetch logs for the new space without resetting the whole state.
|
938 |
-
# Resetting seems safer to avoid confusion.
|
939 |
print(f"Poller detected space name change from {current_project_state.get('repo_id')} to {rid}. Resetting state.")
|
940 |
current_project_state = {
|
941 |
'repo_id': rid,
|
942 |
'iframe_url': url,
|
943 |
'logs': {'build': 'Space name changed, fetching logs...', 'run': 'Space name changed, fetching logs...'},
|
944 |
'iframe_ok': False,
|
945 |
-
'log_error_state': 'None',
|
946 |
'chat_history': [{"role": "assistant", "content": f"Space name changed to **{rid}**. Project state reset. Enter requirements to start development on this Space."}],
|
947 |
'status': 'Not Started',
|
948 |
'status_message': 'Space name changed. Enter requirements to start.',
|
949 |
-
'attempt_count': 0,
|
950 |
'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
|
951 |
}
|
952 |
else:
|
953 |
-
# If space name matches the one in state, update status message
|
954 |
-
if current_project_state.get('status')
|
955 |
-
|
956 |
-
|
957 |
-
|
958 |
-
#
|
959 |
-
|
960 |
-
|
961 |
-
|
962 |
-
|
963 |
-
|
964 |
-
# Avoid flooding chat history, maybe just update project_status_md?
|
965 |
-
# Let's update state message, UI will show it.
|
966 |
-
current_project_state['status_message'] = current_status_message
|
967 |
|
968 |
|
969 |
# Fetch logs and check iframe status using the revised functions
|
970 |
-
|
971 |
-
|
972 |
-
|
973 |
-
|
974 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
975 |
|
976 |
-
# Only fetch logs/check iframe if the project is 'In Progress' and the task is 'LOGGING',
|
977 |
-
# OR if the status is 'Not Started' but a space name is entered (to show logs immediately).
|
978 |
-
# OR if status is Complete/Failed, to allow checking final state.
|
979 |
-
is_orchestration_polling = current_project_state.get('status') == 'In Progress' and current_project_state.get('current_task') == 'LOGGING'
|
980 |
-
is_initial_or_finished_poll = current_project_state.get('status') in ['Not Started', 'Complete', 'Failed']
|
981 |
|
982 |
-
|
983 |
-
|
984 |
-
|
985 |
-
|
986 |
-
|
987 |
-
|
988 |
-
|
989 |
-
elif current_log_error_state == 'None' and build_logs_content.strip() == "No build logs found yet.":
|
990 |
-
current_log_error_state = 'No Logs Yet' # Indicate waiting
|
991 |
-
elif current_log_error_state == 'No Logs Yet' and build_logs_content.strip() != "No build logs found yet.":
|
992 |
-
current_log_error_state = 'None' # Logs started appearing
|
993 |
-
|
994 |
-
except Exception as e: # Should be caught by fetch_logs internal try/except
|
995 |
-
build_logs_content = f"Poller Unexpected error fetching build logs: {e}"
|
996 |
-
current_log_error_state = 'Poller Unexpected Error'
|
997 |
-
print(build_logs_content)
|
998 |
-
|
999 |
-
|
1000 |
-
# Only fetch run logs if a critical error wasn't found for build logs immediately
|
1001 |
-
if current_log_error_state not in ['Auth Failed JWT', 'Not Found', 'Request Error', 'Poller Unexpected Error']:
|
1002 |
-
try:
|
1003 |
-
run_logs_content = fetch_logs(rid, "run")
|
1004 |
-
if run_logs_content.startswith("ERROR_"):
|
1005 |
-
current_log_error_state = run_logs_content.split(':')[0].replace('ERROR_', '') # Overwrite if run log error is more specific
|
1006 |
-
elif current_log_error_state == 'No Logs Yet' and run_logs_content.strip() == "No run logs found yet.":
|
1007 |
-
pass # Keep 'No Logs Yet'
|
1008 |
-
elif current_log_error_state == 'No Logs Yet' and run_logs_content.strip() != "No run logs found yet.":
|
1009 |
-
current_log_error_state = 'None' # Logs started appearing
|
1010 |
-
|
1011 |
-
except Exception as e:
|
1012 |
-
run_logs_content = f"Poller Unexpected error fetching run logs: {e}"
|
1013 |
-
if current_log_error_state == 'None' or current_log_error_state == 'No Logs Yet':
|
1014 |
-
current_log_error_state = 'Poller Unexpected Error'
|
1015 |
-
print(run_logs_content)
|
1016 |
-
else:
|
1017 |
-
run_logs_content = f"Skipped fetching run logs due to build log error state: {current_log_error_state}"
|
1018 |
-
|
1019 |
-
|
1020 |
-
try:
|
1021 |
-
iframe_status_ok = check_iframe(url)
|
1022 |
-
if not iframe_status_ok and current_log_error_state == 'None':
|
1023 |
-
# If iframe isn't OK but no log error, assume it's still building or subtle issue
|
1024 |
-
current_log_error_state = 'Iframe Not Ready'
|
1025 |
-
elif iframe_status_ok and current_log_error_state == 'Iframe Not Ready':
|
1026 |
-
current_log_error_state = 'None' # Iframe is now ready
|
1027 |
-
|
1028 |
-
except Exception as e:
|
1029 |
-
iframe_status_ok = False
|
1030 |
-
if current_log_error_state == 'None':
|
1031 |
-
current_log_error_state = 'Iframe Check Error'
|
1032 |
-
print(f"Poller error checking iframe {url}: {e}")
|
1033 |
else:
|
1034 |
-
# If
|
1035 |
-
|
1036 |
-
|
|
|
|
|
|
|
1037 |
|
1038 |
|
|
|
1039 |
preview_html = (
|
1040 |
-
f'<iframe src="{
|
1041 |
-
if
|
1042 |
-
f"<p style='color:red;'>⚠️ App preview not loading or check failed ({
|
1043 |
-
f"{' **Authentication Error:** Ensure HF_TOKEN secret is set on the Space running this app.' if
|
1044 |
-
f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if
|
1045 |
-
f"{' **Timeout:** Log fetch timed out.' if
|
1046 |
-
f"{' **Iframe not ready:** Still building or error. Check logs.' if
|
1047 |
-
f"{' **Polling Error:** Encountered error fetching logs.' if
|
1048 |
-
f"{'
|
|
|
1049 |
f"</p>"
|
1050 |
)
|
1051 |
|
1052 |
-
# Update the project state with the latest info from the poller
|
1053 |
-
# Only update if the state exists and matches the current repo_id being polled
|
1054 |
-
if current_project_state is not None and current_project_state.get('repo_id') == rid:
|
1055 |
-
current_project_state['logs']['build'] = build_logs_content
|
1056 |
-
current_project_state['logs']['run'] = run_logs_content
|
1057 |
-
current_project_state['iframe_ok'] = iframe_status_ok
|
1058 |
-
current_project_state['log_error_state'] = current_log_error_state
|
1059 |
-
# Do NOT update chat_history or status_message from the poller here,
|
1060 |
-
# as the orchestrator handles those for better flow control.
|
1061 |
-
# The UI will display the *state's* status_message and chat_history.
|
1062 |
|
1063 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1064 |
|
1065 |
|
1066 |
# --- MAIN HANDLER (Called by Gradio) ---
|
1067 |
# Updated signature to include login state explicitly and project state first
|
|
|
|
|
1068 |
def handle_user_message(
|
1069 |
-
profile: gr.OAuthProfile | None, # (1) -
|
1070 |
-
oauth_token: gr.OAuthToken | None, # (2) -
|
1071 |
-
project_state: dict | None, # (3) - from gr.State
|
1072 |
user_input: str, # (4)
|
1073 |
sdk_choice: str, # (5)
|
1074 |
gemini_api_key: str, # (6)
|
@@ -1077,15 +1072,17 @@ def handle_user_message(
|
|
1077 |
temperature: float, # (9)
|
1078 |
max_output_tokens: int, # (10)
|
1079 |
):
|
1080 |
-
# Now the function
|
1081 |
-
# The inputs list will
|
1082 |
|
1083 |
# Initialize or update project_state based on inputs and previous state
|
1084 |
# Decide whether to start a new project or continue
|
|
|
1085 |
is_new_project_request = (
|
1086 |
project_state is None or # First run
|
1087 |
-
project_state.get('status') in ['Complete', 'Failed'] or # Previous project finished
|
1088 |
(user_input is not None and user_input.strip() != "" and user_input.strip() != project_state.get('requirements', '').strip()) # New requirements provided
|
|
|
|
|
1089 |
)
|
1090 |
|
1091 |
if is_new_project_request:
|
@@ -1098,7 +1095,7 @@ def handle_user_message(
|
|
1098 |
sdk_version = get_sdk_version(sdk_choice)
|
1099 |
|
1100 |
project_state = {
|
1101 |
-
'requirements': user_input,
|
1102 |
'plan': '',
|
1103 |
'files': {},
|
1104 |
'logs': {'build': '', 'run': ''},
|
@@ -1116,60 +1113,79 @@ def handle_user_message(
|
|
1116 |
'iframe_ok': False,
|
1117 |
'log_error_state': 'None',
|
1118 |
}
|
1119 |
-
|
|
|
1120 |
|
1121 |
else:
|
1122 |
-
# Continue existing project state
|
1123 |
print("Continuing existing project...")
|
1124 |
-
# history is part of project_state
|
1125 |
-
# project_state['requirements']
|
1126 |
-
|
1127 |
-
project_state['
|
1128 |
-
project_state['
|
|
|
1129 |
clean_space_name = space_name.strip() if space_name else ''
|
1130 |
-
|
1131 |
-
project_state
|
1132 |
-
|
1133 |
-
|
1134 |
-
|
1135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1136 |
|
1137 |
|
1138 |
# Validation Checks (using received profile/token)
|
1139 |
-
|
1140 |
-
|
1141 |
-
if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != error_msg:
|
1142 |
-
project_state['chat_history'].append({"role":"assistant","content":error_msg})
|
1143 |
-
project_state['status'] = 'Failed' # Mark as failed due to login
|
1144 |
-
project_state['status_message'] = "Login required."
|
1145 |
-
project_state['current_task'] = 'FINISHED' # End orchestration
|
1146 |
|
|
|
|
|
|
|
1147 |
elif not space_name or not space_name.strip():
|
1148 |
-
|
1149 |
-
|
1150 |
-
project_state['chat_history'].append({"role":"assistant","content":msg})
|
1151 |
-
project_state['status'] = 'Failed' # Mark as failed due to missing space name
|
1152 |
-
project_state['status_message'] = "Space name required."
|
1153 |
-
project_state['current_task'] = 'FINISHED' # End orchestration
|
1154 |
-
|
1155 |
elif not gemini_api_key:
|
1156 |
-
|
1157 |
-
|
1158 |
-
project_state['chat_history'].append({"role":"assistant","content":error_msg})
|
1159 |
-
project_state['status'] = 'Failed' # Mark as failed due to missing API key
|
1160 |
-
project_state['status_message'] = "API Key required."
|
1161 |
-
project_state['current_task'] = 'FINISHED' # End orchestration
|
1162 |
-
|
1163 |
elif not user_input or user_input.strip() == "":
|
1164 |
-
|
1165 |
-
|
1166 |
-
|
1167 |
-
|
1168 |
-
|
1169 |
-
|
1170 |
-
|
1171 |
-
|
1172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1173 |
if project_state.get('status') == 'In Progress':
|
1174 |
client = genai.Client(api_key=gemini_api_key)
|
1175 |
|
@@ -1181,10 +1197,11 @@ def handle_user_message(
|
|
1181 |
)
|
1182 |
|
1183 |
# Run *one step* of orchestration. orchestrate_development updates project_state directly.
|
1184 |
-
# The function is designed to be called repeatedly
|
|
|
1185 |
try:
|
1186 |
orchestrate_development(
|
1187 |
-
client, project_state, cfg, oauth_token.token
|
1188 |
)
|
1189 |
except Exception as e:
|
1190 |
# Catch unexpected errors during orchestration step
|
@@ -1213,8 +1230,9 @@ def handle_user_message(
|
|
1213 |
f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if project_state.get('log_error_state') == 'Not Found' else ''}"
|
1214 |
f"{' **Timeout:** Log fetch timed out.' if project_state.get('log_error_state') == 'TIMEOUT' else ''}"
|
1215 |
f"{' **Iframe not ready:** Still building or error. Check logs.' if project_state.get('log_error_state') == 'Iframe Not Ready' else ''}"
|
1216 |
-
f"{' **Polling Error:** Encountered error fetching logs.' if project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error'] else ''}"
|
1217 |
-
f"{'
|
|
|
1218 |
f"</p>"),
|
1219 |
project_state.get('status_message', '...'), # Status message output (get from state)
|
1220 |
)
|
@@ -1238,17 +1256,19 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
|
|
1238 |
models_md = gr.Markdown()
|
1239 |
|
1240 |
# Use api_name to make these callable via API if needed, but not strictly necessary for UI load
|
|
|
1241 |
demo.load(show_profile, inputs=None, outputs=status_md) # api_name="load_profile" removed
|
1242 |
demo.load(list_private_models, inputs=None, outputs=models_md) # api_name="load_models" removed
|
1243 |
|
|
|
1244 |
login_btn.click(
|
1245 |
fn=show_profile,
|
1246 |
-
inputs=None,
|
1247 |
outputs=status_md
|
1248 |
)
|
1249 |
login_btn.click(
|
1250 |
fn=list_private_models,
|
1251 |
-
inputs=None,
|
1252 |
outputs=models_md
|
1253 |
)
|
1254 |
# --- END LOGIN FIX ---
|
@@ -1283,17 +1303,15 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
|
|
1283 |
preview = gr.HTML("<p>App preview will load here when available.</p>")
|
1284 |
|
1285 |
# --- Hidden poller (using Timer) ---
|
1286 |
-
# Polls
|
1287 |
-
# Constant 2s is fine for demonstration.
|
1288 |
log_poller = gr.Timer(value=2, active=True, render=False)
|
1289 |
# --- End hidden poller ---
|
1290 |
|
1291 |
# handle_user_message is defined ABOVE this block
|
1292 |
# The main button and submit handler now receive and return the project_state
|
1293 |
-
# INPUTS:
|
1294 |
inputs_list = [
|
1295 |
-
|
1296 |
-
project_state, # <-- Pass the state in
|
1297 |
user_in, # Other UI inputs in order
|
1298 |
sdk_choice,
|
1299 |
api_key,
|
@@ -1301,8 +1319,9 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
|
|
1301 |
grounding,
|
1302 |
temp,
|
1303 |
max_tokens,
|
|
|
1304 |
# Note: chatbot is NOT in inputs, its history is managed via project_state['chat_history']
|
1305 |
-
] # Total
|
1306 |
|
1307 |
# OUTPUTS: Must match the return values of handle_user_message
|
1308 |
outputs_list = [
|
@@ -1318,23 +1337,27 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
|
|
1318 |
fn=handle_user_message,
|
1319 |
inputs=inputs_list,
|
1320 |
outputs=outputs_list
|
|
|
1321 |
)
|
1322 |
|
1323 |
user_in.submit(
|
1324 |
fn=handle_user_message,
|
1325 |
inputs=inputs_list,
|
1326 |
outputs=outputs_list
|
|
|
1327 |
)
|
1328 |
|
1329 |
# --- Wire the poller to the update function ---
|
1330 |
# _update_logs_and_preview is defined ABOVE this block
|
1331 |
-
# The poller needs login state, space name, and the current project state
|
1332 |
# INPUTS: login_btn state (as tuple), space name, project state
|
|
|
|
|
1333 |
poller_inputs = [
|
1334 |
-
login_btn, # Provides (profile, token) as a tuple
|
1335 |
space_name,
|
1336 |
project_state # Passes the current state value
|
1337 |
-
] # Total 3 components ->
|
1338 |
# Note: _update_logs_and_preview signature is (profile_token_state, space_name, current_project_state)
|
1339 |
|
1340 |
# OUTPUTS: Must match the return values of _update_logs_and_preview
|
@@ -1349,6 +1372,7 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
|
|
1349 |
fn=_update_logs_and_preview,
|
1350 |
inputs=poller_inputs,
|
1351 |
outputs=poller_outputs
|
|
|
1352 |
)
|
1353 |
# --- End wire poller ---
|
1354 |
|
@@ -1360,4 +1384,5 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
|
|
1360 |
# If running on a Space, server_name='0.0.0.0' and server_port=7860 are standard.
|
1361 |
# If running locally, just demo.launch() or specify a port.
|
1362 |
if __name__ == "__main__":
|
1363 |
-
|
|
|
|
517 |
project_state['files'] = {}
|
518 |
project_state['iframe_ok'] = False
|
519 |
project_state['log_error_state'] = 'None'
|
520 |
+
# Initial message handled in handle_user_message now
|
521 |
+
# project_state['chat_history'].append({"role": "assistant", "content": "Project initialized. Starting development team."})
|
522 |
return # Exit after initialization, next call will run the first task
|
523 |
|
524 |
if project_state['status'] != 'In Progress':
|
|
|
536 |
if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != task_message.strip():
|
537 |
project_state['chat_history'].append({"role": "assistant", "content": task_message})
|
538 |
|
539 |
+
# step_successful is managed within each task block
|
540 |
|
541 |
if current_task == 'START':
|
542 |
# This state should only be hit once for initialization, handled above.
|
|
|
600 |
project_state['current_task'] = 'PUSHING'
|
601 |
# Clear previous logs and feedback before pushing a new version
|
602 |
project_state['logs'] = {'build': '', 'run': ''}
|
603 |
+
project_state['feedback'] = '' # Clear feedback from previous debug cycle
|
604 |
project_state['iframe_ok'] = False
|
605 |
project_state['log_error_state'] = 'None' # Reset log error state
|
606 |
# Do NOT reset attempt count here, it's for total cycles
|
|
|
616 |
project_state['status_message'] = "ERROR: Cannot push without Hugging Face token."
|
617 |
project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
|
618 |
project_state['current_task'] = 'FINISHED'
|
|
|
619 |
print(project_state['status_message'])
|
620 |
return # Exit early on critical error
|
621 |
|
|
|
642 |
|
643 |
# Use a temporary directory to avoid cluttering the working directory
|
644 |
import tempfile
|
645 |
+
step_successful = True # Assume success initially for pushing
|
646 |
with tempfile.TemporaryDirectory() as tmpdir:
|
647 |
temp_file_paths = []
|
648 |
for fn, content in files_to_push.items():
|
|
|
655 |
temp_file_paths.append(full_path)
|
656 |
except Exception as e:
|
657 |
print(f"Error writing temporary file {fn}: {e}")
|
658 |
+
# Decide how to handle: skip file? Fail push? For now, mark push as failed.
|
659 |
+
step_successful = False # Mark push as failed if any file fails to write
|
660 |
project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError writing file {fn} for push: {e}"
|
661 |
+
project_state['status_message'] = f"ERROR: Failed to write {fn} locally for push. Details in feedback."
|
662 |
|
663 |
|
664 |
+
if not temp_file_paths and len(files_to_push) > 0: # If files were supposed to be pushed but none were written
|
665 |
print("No valid files prepared for push.")
|
666 |
+
project_state['status_message'] = "ERROR: No files were prepared for push after Code-Gen. Project Failed."
|
667 |
project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
|
668 |
project_state['status'] = 'Failed'
|
669 |
project_state['current_task'] = 'FINISHED'
|
670 |
step_successful = False
|
671 |
+
# No need to return here, let the rest of the block handle the failure state
|
672 |
+
|
673 |
+
|
674 |
+
# Upload files one by one IF step_successful is still True after writing
|
675 |
+
if step_successful:
|
676 |
+
for fn in files_to_push.keys(): # Iterate over keys to get path_in_repo
|
677 |
+
filepath = os.path.join(tmpdir, fn)
|
678 |
+
# Only try to upload if temp file was successfully written and overall push isn't already failed
|
679 |
+
if filepath in temp_file_paths and step_successful:
|
680 |
+
try:
|
681 |
+
upload_file(
|
682 |
+
path_or_fileobj=filepath, path_in_repo=fn,
|
683 |
+
repo_id=repo_id, token=oauth_token_token,
|
684 |
+
repo_type="space"
|
685 |
+
)
|
686 |
+
print(f"Uploaded: {fn}")
|
687 |
+
except Exception as e:
|
688 |
+
print(f"Error uploading file {fn}: {e}")
|
689 |
+
step_successful = False # Mark push as failed if any upload fails
|
690 |
+
project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError uploading {fn}: {e}"
|
691 |
+
project_state['status_message'] = f"ERROR: Failed to upload {fn}. Details in feedback."
|
692 |
+
# Continue trying other files, but mark overall step as failed
|
693 |
+
elif step_successful: # If step_successful is True but file wasn't written
|
694 |
+
print(f"Skipping upload for {fn} as it failed to write locally.")
|
695 |
+
# This case is already covered by the write loop failing step_successful
|
696 |
+
|
697 |
|
698 |
if step_successful:
|
699 |
project_state['status_message'] = f"Pushed code to HF Space **{repo_id}**. Build triggered. Waiting for build and logs..."
|
|
|
703 |
# Logs, iframe_ok, log_error_state were reset before push, will be updated by poller
|
704 |
|
705 |
else:
|
706 |
+
# If any write or upload failed, the push step failed
|
707 |
project_state['status'] = 'Failed'
|
708 |
project_state['current_task'] = 'FINISHED'
|
709 |
+
project_state['status_message'] = project_state.get('status_message', f"ERROR: One or more files failed to process or upload to HF Space {repo_id}. See feedback.")
|
710 |
+
# Ensure the error message is added to chat history if not already there from writing/uploading
|
711 |
+
if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != project_state['status_message'].strip():
|
712 |
+
project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
|
713 |
print(project_state['status_message'])
|
714 |
|
715 |
|
|
|
756 |
project_state['status_message'] = "Iframe check passed. Analyzing build/run logs."
|
757 |
|
758 |
# Check for explicit log fetching errors
|
759 |
+
elif log_error_state not in ['None', 'No Logs Yet', 'Iframe Not Ready']: # Any error state that is NOT just 'waiting'
|
760 |
print(f"Orchestrator: Detected log fetching error state: {log_error_state}. Moving to Debugging.")
|
761 |
ready_to_debug = True
|
762 |
project_state['status_message'] = f"Log fetching encountered an error ({log_error_state}). Moving to Debugging."
|
763 |
|
764 |
# Check for significant logs that indicate build/run started/failed
|
765 |
# Avoid triggering debug if it's just the 'No logs found yet' message
|
766 |
+
elif not (build_logs.strip() == "No build logs found yet." and run_logs.strip() == "No run logs found yet."):
|
767 |
# Check for actual log content or error indicators within logs
|
768 |
+
if len(build_logs.strip()) > 50 or len(run_logs.strip()) > 10 or "ERROR" in build_logs.upper() or "FATAL" in build_logs.upper() or "ERROR" in run_logs.upper() or "FATAL" in run_logs.upper():
|
769 |
print("Orchestrator: Detected significant logs. Moving to Debugging.")
|
770 |
ready_to_debug = True
|
771 |
project_state['status_message'] = "Significant logs detected. Analyzing build/run logs."
|
|
|
774 |
# Timeout check - Use attempt count as a global timeout for the entire process,
|
775 |
# but also use it to limit time spent *just* in LOGGING if no progress is detected.
|
776 |
if project_state['attempt_count'] >= 6: # Use attempt count as a global timeout trigger
|
777 |
+
print("Orchestrator: Max attempts reached in LOGGING/overall. Forcing move to Debugging.")
|
778 |
ready_to_debug = True # Force debug to get final feedback before failing
|
779 |
|
780 |
|
781 |
if ready_to_debug:
|
782 |
project_state['current_task'] = 'DEBUGGING'
|
783 |
# Add a small delay before debugging starts, giving UI time to update logs
|
784 |
+
# time.sleep(2) # Moved implicit wait to poller tick
|
|
|
785 |
else:
|
786 |
# If not ready to debug, stay in LOGGING. The orchestrator loop will naturally pause between calls.
|
787 |
+
# Attempt count is incremented globally at the end of the loop if not finished/failed.
|
|
|
|
|
788 |
pass # Stay in LOGGING, loop will repeat
|
789 |
|
790 |
|
|
|
823 |
is_failed = True
|
824 |
project_state['status_message'] = f"Max attempts ({project_state['attempt_count']+1}/7) reached. Project failed."
|
825 |
print(f"Debug Analysis: Failed due to max attempts ({project_state['attempt_count']+1}).")
|
826 |
+
elif log_error_state in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Request Error', 'Unexpected Error', 'Poller Unexpected Error', 'HTTP Error']: # Critical log fetching errors
|
827 |
is_failed = True
|
828 |
+
project_state['status_message'] = f"Project failed due to critical log or iframe fetching error: {log_error_state}"
|
829 |
print(f"Debug Analysis: Failed due to critical log error: {log_error_state}.")
|
830 |
elif ("ERROR" in feedback.upper() or error_types != "none") and project_state['attempt_count'] >= 4:
|
831 |
# If debugger finds errors or classified errors exist after several attempts (e.g., attempts 5, 6, 7)
|
|
|
863 |
|
864 |
else:
|
865 |
# Should not happen - unknown state
|
866 |
+
# step_successful = False # Force failure path - not needed as status is set
|
867 |
project_state['status'] = 'Failed'
|
868 |
project_state['status_message'] = f"ERROR: Orchestrator entered an unknown task state: {current_task}"
|
869 |
project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
|
|
|
879 |
# time.sleep(1) # Adding a 1-second pause between steps
|
880 |
|
881 |
# Final outcome message when status changes to Complete or Failed
|
882 |
+
# Only add if it's the final state and the message hasn't been added
|
883 |
if project_state['status'] != 'In Progress' and not any(msg.get('content', '').strip().startswith("**Project Outcome:") for msg in project_state['chat_history']):
|
884 |
final_outcome_message = f"**Project Outcome:** {project_state['status']} - {project_state.get('status_message', 'No specific outcome message.')}"
|
885 |
project_state['chat_history'].append({"role": "assistant", "content": final_outcome_message})
|
|
|
894 |
project_state['chat_history'].append({"role": "assistant", "content": failure_message})
|
895 |
|
896 |
|
897 |
+
# orchestrate_development function doesn't return UI outputs directly anymore.
|
898 |
# It updates the project_state dictionary, which is a gr.State,
|
899 |
# and the UI outputs are updated by the poller (_update_logs_and_preview)
|
900 |
# or by the main handle_user_message function after calling orchestrate_development.
|
|
|
923 |
rid = f"{profile.username}/{clean}"
|
924 |
url = f"https://huggingface.co/spaces/{profile.username}/{clean}"
|
925 |
|
926 |
+
# Initialize project_state if it's the first call or if space name changed while not mid-project
|
|
|
927 |
if current_project_state is None:
|
928 |
+
print(f"Poller: Initializing state for space {rid}")
|
929 |
current_project_state = {
|
930 |
'repo_id': rid, # Set repo_id based on current UI inputs
|
931 |
'iframe_url': url, # Set iframe_url based on current UI inputs
|
932 |
+
'logs': {'build': 'Polling...', 'run': 'Polling...'},
|
933 |
'iframe_ok': False,
|
934 |
+
'log_error_state': 'Polling...', # Initial polling state
|
935 |
+
'chat_history': [], # Start with empty history for a new space
|
936 |
'status': 'Not Started',
|
937 |
+
'status_message': f'Polling logs for {rid}... Enter requirements to start project.',
|
938 |
'attempt_count': 0,
|
939 |
'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
|
940 |
}
|
941 |
+
# Add an initial message if history is empty
|
942 |
+
if not current_project_state['chat_history']:
|
943 |
+
current_project_state['chat_history'].append({"role": "assistant", "content": current_project_state['status_message']})
|
944 |
+
|
945 |
elif current_project_state.get('repo_id') != rid:
|
946 |
# If the UI Space name changes while a project is in progress for a *different* space,
|
947 |
# reset the project state to focus on the new space name.
|
|
|
|
|
948 |
print(f"Poller detected space name change from {current_project_state.get('repo_id')} to {rid}. Resetting state.")
|
949 |
current_project_state = {
|
950 |
'repo_id': rid,
|
951 |
'iframe_url': url,
|
952 |
'logs': {'build': 'Space name changed, fetching logs...', 'run': 'Space name changed, fetching logs...'},
|
953 |
'iframe_ok': False,
|
954 |
+
'log_error_state': 'None', # Reset error state
|
955 |
'chat_history': [{"role": "assistant", "content": f"Space name changed to **{rid}**. Project state reset. Enter requirements to start development on this Space."}],
|
956 |
'status': 'Not Started',
|
957 |
'status_message': 'Space name changed. Enter requirements to start.',
|
958 |
+
'attempt_count': 0, # Reset attempts for a new project
|
959 |
'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
|
960 |
}
|
961 |
else:
|
962 |
+
# If space name matches the one in state, potentially update polling status message
|
963 |
+
if current_project_state.get('status') in ['Not Started']:
|
964 |
+
current_status_message = f"Polling logs for {rid}... Current status: {current_project_state.get('status_message', '...')}"
|
965 |
+
# Avoid flooding chat history with this, just update the status message field
|
966 |
+
current_project_state['status_message'] = current_status_message
|
967 |
+
# Ensure there's at least an initial message in history if needed
|
968 |
+
if not current_project_state['chat_history']:
|
969 |
+
current_project_state['chat_history'].append({"role": "assistant", "content": current_project_state['status_message']})
|
970 |
+
# If status is 'In Progress' and task is 'LOGGING', the orchestrator manages status_message.
|
971 |
+
# If status is 'Complete' or 'Failed', the final status_message is set.
|
972 |
+
# Otherwise, keep the current status_message.
|
|
|
|
|
|
|
973 |
|
974 |
|
975 |
# Fetch logs and check iframe status using the revised functions
|
976 |
+
# Always attempt to fetch logs and check iframe if a repo_id is set in the state.
|
977 |
+
# The fetch_logs function handles errors like 401/404/timeout.
|
978 |
+
if current_project_state.get('repo_id'):
|
979 |
+
print(f"Poller fetching logs for {rid} (Status: {current_project_state.get('status')}, Task: {current_project_state.get('current_task')})...")
|
980 |
|
981 |
+
build_logs_content = fetch_logs(rid, "build")
|
982 |
+
run_logs_content = fetch_logs(rid, "run") # Fetch run logs regardless of build log status
|
983 |
+
iframe_status_ok = check_iframe(url)
|
984 |
+
|
985 |
+
# Update log_error_state based on fetch results
|
986 |
+
# Priority to critical errors > iframe issues > waiting
|
987 |
+
new_log_error_state = 'None'
|
988 |
+
|
989 |
+
if build_logs_content.startswith("ERROR_AUTH_FAILED_JWT:") or run_logs_content.startswith("ERROR_AUTH_FAILED_JWT:"):
|
990 |
+
new_log_error_state = 'Auth Failed JWT'
|
991 |
+
elif build_logs_content.startswith("ERROR_AUTH_FAILED_STREAM:") or run_logs_content.startswith("ERROR_AUTH_FAILED_STREAM:"):
|
992 |
+
new_log_error_state = 'Auth Failed STREAM'
|
993 |
+
elif build_logs_content.startswith("ERROR_NOT_FOUND:") or run_logs_content.startswith("ERROR_NOT_FOUND:"):
|
994 |
+
new_log_error_state = 'Not Found'
|
995 |
+
elif build_logs_content.startswith("ERROR_TIMEOUT:") or run_logs_content.startswith("ERROR_TIMEOUT:"):
|
996 |
+
new_log_error_state = 'TIMEOUT'
|
997 |
+
elif build_logs_content.startswith("ERROR_REQUEST:") or run_logs_content.startswith("ERROR_REQUEST:"):
|
998 |
+
new_log_error_state = 'Request Error'
|
999 |
+
elif build_logs_content.startswith("ERROR_HTTP_") or run_logs_content.startswith("ERROR_HTTP_"):
|
1000 |
+
new_log_error_state = 'HTTP Error'
|
1001 |
+
elif build_logs_content.startswith("ERROR_UNEXPECTED:") or run_logs_content.startswith("ERROR_UNEXPECTED:"):
|
1002 |
+
new_log_error_state = 'Poller Unexpected Error'
|
1003 |
+
elif not iframe_status_ok:
|
1004 |
+
# If iframe is not ok, and no explicit log error, assume it's not ready
|
1005 |
+
new_log_error_state = 'Iframe Not Ready'
|
1006 |
+
elif build_logs_content.strip() == "No build logs found yet." and run_logs_content.strip() == "No run logs found yet.":
|
1007 |
+
# If no logs and iframe ok, maybe it's a very simple app? Or a very fast build?
|
1008 |
+
# Let's not mark it as 'No Logs Yet' if iframe is OK.
|
1009 |
+
pass # Keep None
|
1010 |
+
elif (build_logs_content.strip() == "No build logs found yet." or run_logs_content.strip() == "No run logs found yet.") and iframe_status_ok:
|
1011 |
+
# Partial logs, but iframe is OK
|
1012 |
+
pass # Keep None
|
1013 |
+
elif (build_logs_content.strip() != "No build logs found yet." or run_logs_content.strip() != "No run logs found yet.") and not iframe_status_ok:
|
1014 |
+
# Logs are appearing but iframe is not OK
|
1015 |
+
new_log_error_state = 'Iframe Not Ready' # Re-assert iframe not ready if logs appear but it's still down
|
1016 |
|
|
|
|
|
|
|
|
|
|
|
1017 |
|
1018 |
+
current_project_state['logs']['build'] = build_logs_content
|
1019 |
+
current_project_state['logs']['run'] = run_logs_content
|
1020 |
+
current_project_state['iframe_ok'] = iframe_status_ok
|
1021 |
+
current_project_state['log_error_state'] = new_log_error_state
|
1022 |
+
# Do NOT update chat_history or status_message from the poller here.
|
1023 |
+
# The orchestrator needs to react to the *state* changes, not manage the UI messages directly from poller.
|
1024 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1025 |
else:
|
1026 |
+
# If repo_id is not set in state, cannot poll logs
|
1027 |
+
build_logs_content = current_project_state['logs'].get('build', 'Space name not set, cannot poll logs.')
|
1028 |
+
run_logs_content = current_project_state['logs'].get('run', 'Space name not set, cannot poll logs.')
|
1029 |
+
iframe_status_ok = False
|
1030 |
+
current_log_error_state = current_project_state.get('log_error_state', 'Space Not Set')
|
1031 |
+
print("Poller skipping log fetch because repo_id is not set in state.")
|
1032 |
|
1033 |
|
1034 |
+
# Reconstruct preview HTML based on the updated state
|
1035 |
preview_html = (
|
1036 |
+
f'<iframe src="{current_project_state.get("iframe_url", "")}" width="100%" height="500px" allow="accelerometer; ambient-light-sensor; autoplay; camera; gyroscope; hid; fullscreen; illustration; xr-spatial-tracking; sync-xhr;" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" frameborder="0"></iframe>'
|
1037 |
+
if current_project_state.get('iframe_ok') else
|
1038 |
+
f"<p style='color:red;'>⚠️ App preview not loading or check failed ({current_project_state.get('iframe_url', 'URL not set.')})."
|
1039 |
+
f"{' **Authentication Error:** Ensure HF_TOKEN secret is set on the Space running this app.' if current_project_state.get('log_error_state') in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM'] else ''}"
|
1040 |
+
f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if current_project_state.get('log_error_state') == 'Not Found' else ''}"
|
1041 |
+
f"{' **Timeout:** Log fetch timed out.' if current_project_state.get('log_error_state') == 'TIMEOUT' else ''}"
|
1042 |
+
f"{' **Iframe not ready:** Still building or error. Check logs.' if current_project_state.get('log_error_state') == 'Iframe Not Ready' else ''}"
|
1043 |
+
f"{' **Polling Error:** Encountered error fetching logs.' if current_project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error', 'HTTP Error'] else ''}"
|
1044 |
+
f"{' **Space Name Not Set:** Cannot fetch logs or check iframe.' if current_project_state.get('log_error_state') == 'Space Not Set' else ''}"
|
1045 |
+
f"{' Check build logs for details.' if current_project_state.get('log_error_state') not in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Not Found', 'Space Not Set'] else ''}" # Suggest checking logs if no specific critical error
|
1046 |
f"</p>"
|
1047 |
)
|
1048 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1049 |
|
1050 |
+
# Return the updated state along with the UI outputs derived *from* the state
|
1051 |
+
return (
|
1052 |
+
build_logs_content, # Build logs output
|
1053 |
+
run_logs_content, # Run logs output
|
1054 |
+
preview_html, # Preview HTML output
|
1055 |
+
current_project_state # Updated project state
|
1056 |
+
)
|
1057 |
|
1058 |
|
1059 |
# --- MAIN HANDLER (Called by Gradio) ---
|
1060 |
# Updated signature to include login state explicitly and project state first
|
1061 |
+
# Note: Gradio 4.x might prefer profile and token as the first two args when auto-injecting.
|
1062 |
+
# Let's stick to the recommended Gradio pattern where auto-injected params come first.
|
1063 |
def handle_user_message(
|
1064 |
+
profile: gr.OAuthProfile | None, # (1) - Auto-injected by Gradio
|
1065 |
+
oauth_token: gr.OAuthToken | None, # (2) - Auto-injected by Gradio
|
1066 |
+
project_state: dict | None, # (3) - from gr.State component
|
1067 |
user_input: str, # (4)
|
1068 |
sdk_choice: str, # (5)
|
1069 |
gemini_api_key: str, # (6)
|
|
|
1072 |
temperature: float, # (9)
|
1073 |
max_output_tokens: int, # (10)
|
1074 |
):
|
1075 |
+
# Now the function signature has 10 parameters.
|
1076 |
+
# The inputs list will exclude login_btn to allow auto-injection.
|
1077 |
|
1078 |
# Initialize or update project_state based on inputs and previous state
|
1079 |
# Decide whether to start a new project or continue
|
1080 |
+
# A new project starts if state is None, or if requirements change
|
1081 |
is_new_project_request = (
|
1082 |
project_state is None or # First run
|
|
|
1083 |
(user_input is not None and user_input.strip() != "" and user_input.strip() != project_state.get('requirements', '').strip()) # New requirements provided
|
1084 |
+
# We could also check if status is 'Complete' or 'Failed' to auto-reset, but letting
|
1085 |
+
# the user input change trigger the reset is more explicit.
|
1086 |
)
|
1087 |
|
1088 |
if is_new_project_request:
|
|
|
1095 |
sdk_version = get_sdk_version(sdk_choice)
|
1096 |
|
1097 |
project_state = {
|
1098 |
+
'requirements': user_input.strip() if user_input else '', # Store cleaned requirements
|
1099 |
'plan': '',
|
1100 |
'files': {},
|
1101 |
'logs': {'build': '', 'run': ''},
|
|
|
1113 |
'iframe_ok': False,
|
1114 |
'log_error_state': 'None',
|
1115 |
}
|
1116 |
+
if user_input and user_input.strip():
|
1117 |
+
project_state['chat_history'].append({"role": "user", "content": user_input.strip()}) # Add initial user requirement
|
1118 |
|
1119 |
else:
|
1120 |
+
# Continue existing project state
|
1121 |
print("Continuing existing project...")
|
1122 |
+
# history is part of project_state - no need to copy
|
1123 |
+
# project_state['requirements'] stays the same requirement that started the project
|
1124 |
+
# Update other settings that could change mid-project (though maybe shouldn't)
|
1125 |
+
project_state['sdk_choice'] = sdk_choice
|
1126 |
+
project_state['main_app_file'] = "app.py" if sdk_choice == "gradio" else "streamlit_app.py"
|
1127 |
+
project_state['sdk_version'] = get_sdk_version(sdk_choice)
|
1128 |
clean_space_name = space_name.strip() if space_name else ''
|
1129 |
+
# Only update repo_id/iframe_url if the name changes (and state is not 'Not Started')
|
1130 |
+
if project_state.get('repo_id') != f"{profile.username}/{clean_space_name}" and project_state.get('status') != 'Not Started' :
|
1131 |
+
# This case should ideally be handled by the poller's state reset, but double check.
|
1132 |
+
print(f"Handle_user_message detected space name change mid-project from {project_state.get('repo_id')} to {profile.username}/{clean_space_name}. Forcing project reset.")
|
1133 |
+
# Re-initialize as a new project
|
1134 |
+
return handle_user_message(profile, oauth_token, None, user_input, sdk_choice, gemini_api_key, space_name, grounding_enabled, temperature, max_output_tokens)
|
1135 |
+
elif profile and clean_space_name:
|
1136 |
+
project_state['repo_id'] = f"{profile.username}/{clean_space_name}"
|
1137 |
+
project_state['iframe_url'] = f"https://huggingface.co/spaces/{profile.username}/{clean_space_name}"
|
1138 |
+
|
1139 |
+
# If status is 'Not Started' and user sends input again (maybe after fixing something),
|
1140 |
+
# set task to START to begin orchestration.
|
1141 |
+
if project_state.get('status') == 'Not Started' and user_input and user_input.strip() and project_state.get('current_task') is None:
|
1142 |
+
print("Status was 'Not Started', user sent input. Starting orchestration.")
|
1143 |
+
project_state['current_task'] = 'START'
|
1144 |
+
project_state['status'] = 'In Progress'
|
1145 |
+
project_state['status_message'] = 'Initializing...'
|
1146 |
|
1147 |
|
1148 |
# Validation Checks (using received profile/token)
|
1149 |
+
validation_error = None
|
1150 |
+
validation_message = None
|
|
|
|
|
|
|
|
|
|
|
1151 |
|
1152 |
+
if not profile or not oauth_token or not hasattr(oauth_token, 'token') or not oauth_token.token:
|
1153 |
+
validation_error = "Login required."
|
1154 |
+
validation_message = "⚠️ Please log in first via the Hugging Face button."
|
1155 |
elif not space_name or not space_name.strip():
|
1156 |
+
validation_error = "Space name required."
|
1157 |
+
validation_message = "⚠️ Please enter a Space name."
|
|
|
|
|
|
|
|
|
|
|
1158 |
elif not gemini_api_key:
|
1159 |
+
validation_error = "API Key required."
|
1160 |
+
validation_message = "⚠️ Please provide your Gemini API Key."
|
|
|
|
|
|
|
|
|
|
|
1161 |
elif not user_input or user_input.strip() == "":
|
1162 |
+
validation_error = "Requirements required."
|
1163 |
+
validation_message = "Please enter requirements for the application."
|
1164 |
+
|
1165 |
+
|
1166 |
+
# If there's a validation error, update state and return early
|
1167 |
+
if validation_error:
|
1168 |
+
if project_state.get('status') != 'Failed' or project_state.get('status_message') != validation_message:
|
1169 |
+
# Avoid adding duplicate error messages
|
1170 |
+
project_state['status'] = 'Failed' # Mark as failed due to validation
|
1171 |
+
project_state['status_message'] = validation_message
|
1172 |
+
project_state['current_task'] = 'FINISHED' # End orchestration
|
1173 |
+
if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != validation_message:
|
1174 |
+
project_state['chat_history'].append({"role":"assistant","content":validation_message})
|
1175 |
+
|
1176 |
+
# Return current state and UI outputs
|
1177 |
+
return (
|
1178 |
+
project_state,
|
1179 |
+
project_state.get('chat_history', []),
|
1180 |
+
project_state['logs'].get('build', ''),
|
1181 |
+
project_state['logs'].get('run', ''),
|
1182 |
+
f"<p style='color:red;'>{validation_message}</p>", # Simple error preview
|
1183 |
+
project_state.get('status_message', 'Validation Failed')
|
1184 |
+
)
|
1185 |
+
|
1186 |
+
|
1187 |
+
# If validation passed and status is 'In Progress' or being set to 'In Progress' ('START' task)
|
1188 |
+
# Note: orchestrate_development handles the 'START' -> 'In Progress' transition internally
|
1189 |
if project_state.get('status') == 'In Progress':
|
1190 |
client = genai.Client(api_key=gemini_api_key)
|
1191 |
|
|
|
1197 |
)
|
1198 |
|
1199 |
# Run *one step* of orchestration. orchestrate_development updates project_state directly.
|
1200 |
+
# The function is designed to be called repeatedly by subsequent handle_user_message calls
|
1201 |
+
# triggered by the poller implicitly updating state and thus triggering retries.
|
1202 |
try:
|
1203 |
orchestrate_development(
|
1204 |
+
client, project_state, cfg, oauth_token.token # Pass the token for pushing
|
1205 |
)
|
1206 |
except Exception as e:
|
1207 |
# Catch unexpected errors during orchestration step
|
|
|
1230 |
f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if project_state.get('log_error_state') == 'Not Found' else ''}"
|
1231 |
f"{' **Timeout:** Log fetch timed out.' if project_state.get('log_error_state') == 'TIMEOUT' else ''}"
|
1232 |
f"{' **Iframe not ready:** Still building or error. Check logs.' if project_state.get('log_error_state') == 'Iframe Not Ready' else ''}"
|
1233 |
+
f"{' **Polling Error:** Encountered error fetching logs.' if project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error', 'HTTP Error'] else ''}"
|
1234 |
+
f"{' **Space Name Not Set:** Cannot fetch logs or check iframe.' if project_state.get('log_error_state') == 'Space Not Set' else ''}"
|
1235 |
+
f"{' Check build logs for details.' if project_state.get('log_error_state') not in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Not Found', 'Space Not Set'] else ''}" # Suggest checking logs if no specific critical error
|
1236 |
f"</p>"),
|
1237 |
project_state.get('status_message', '...'), # Status message output (get from state)
|
1238 |
)
|
|
|
1256 |
models_md = gr.Markdown()
|
1257 |
|
1258 |
# Use api_name to make these callable via API if needed, but not strictly necessary for UI load
|
1259 |
+
# Ensure these load on initial page load
|
1260 |
demo.load(show_profile, inputs=None, outputs=status_md) # api_name="load_profile" removed
|
1261 |
demo.load(list_private_models, inputs=None, outputs=models_md) # api_name="load_models" removed
|
1262 |
|
1263 |
+
# Ensure these update on login button click
|
1264 |
login_btn.click(
|
1265 |
fn=show_profile,
|
1266 |
+
inputs=None, # OAuth auto-injected
|
1267 |
outputs=status_md
|
1268 |
)
|
1269 |
login_btn.click(
|
1270 |
fn=list_private_models,
|
1271 |
+
inputs=None, # OAuth auto-injected
|
1272 |
outputs=models_md
|
1273 |
)
|
1274 |
# --- END LOGIN FIX ---
|
|
|
1303 |
preview = gr.HTML("<p>App preview will load here when available.</p>")
|
1304 |
|
1305 |
# --- Hidden poller (using Timer) ---
|
1306 |
+
# Polls every 2 seconds to update logs and preview
|
|
|
1307 |
log_poller = gr.Timer(value=2, active=True, render=False)
|
1308 |
# --- End hidden poller ---
|
1309 |
|
1310 |
# handle_user_message is defined ABOVE this block
|
1311 |
# The main button and submit handler now receive and return the project_state
|
1312 |
+
# INPUTS: relies on auto-injection of profile/token, then state, then other UI inputs
|
1313 |
inputs_list = [
|
1314 |
+
project_state, # <-- Pass the state in (after auto-injected profile/token)
|
|
|
1315 |
user_in, # Other UI inputs in order
|
1316 |
sdk_choice,
|
1317 |
api_key,
|
|
|
1319 |
grounding,
|
1320 |
temp,
|
1321 |
max_tokens,
|
1322 |
+
# Note: login_btn is NOT in inputs, profile/token are auto-injected
|
1323 |
# Note: chatbot is NOT in inputs, its history is managed via project_state['chat_history']
|
1324 |
+
] # Total 8 components listed here, plus 2 auto-injected = 10 arguments
|
1325 |
|
1326 |
# OUTPUTS: Must match the return values of handle_user_message
|
1327 |
outputs_list = [
|
|
|
1337 |
fn=handle_user_message,
|
1338 |
inputs=inputs_list,
|
1339 |
outputs=outputs_list
|
1340 |
+
# No api_name needed unless you plan to call this via API directly
|
1341 |
)
|
1342 |
|
1343 |
user_in.submit(
|
1344 |
fn=handle_user_message,
|
1345 |
inputs=inputs_list,
|
1346 |
outputs=outputs_list
|
1347 |
+
# No api_name needed
|
1348 |
)
|
1349 |
|
1350 |
# --- Wire the poller to the update function ---
|
1351 |
# _update_logs_and_preview is defined ABOVE this block
|
1352 |
+
# The poller needs login state (as tuple), space name, and the current project state
|
1353 |
# INPUTS: login_btn state (as tuple), space name, project state
|
1354 |
+
# When login_btn is the *first* component in the inputs list and the function's
|
1355 |
+
# first parameter is a tuple of the correct types, Gradio passes the outputs as a tuple.
|
1356 |
poller_inputs = [
|
1357 |
+
login_btn, # Provides (profile, token) as a tuple because it's first
|
1358 |
space_name,
|
1359 |
project_state # Passes the current state value
|
1360 |
+
] # Total 3 components -> function receives 3 arguments (1 tuple + 1 + 1)
|
1361 |
# Note: _update_logs_and_preview signature is (profile_token_state, space_name, current_project_state)
|
1362 |
|
1363 |
# OUTPUTS: Must match the return values of _update_logs_and_preview
|
|
|
1372 |
fn=_update_logs_and_preview,
|
1373 |
inputs=poller_inputs,
|
1374 |
outputs=poller_outputs
|
1375 |
+
# No api_name needed
|
1376 |
)
|
1377 |
# --- End wire poller ---
|
1378 |
|
|
|
1384 |
# If running on a Space, server_name='0.0.0.0' and server_port=7860 are standard.
|
1385 |
# If running locally, just demo.launch() or specify a port.
|
1386 |
if __name__ == "__main__":
|
1387 |
+
# Use `show_error=True` to see the actual Python errors in the UI during development
|
1388 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|