Joseph Pollack commited on
Commit
8fa2ce6
·
unverified ·
1 Parent(s): a2f220d

attempts fix 403 and settings

Browse files
.pre-commit-config.yaml CHANGED
@@ -14,7 +14,7 @@ repos:
14
  hooks:
15
  - id: mypy
16
  files: ^src/
17
- exclude: ^folder
18
  additional_dependencies:
19
  - pydantic>=2.7
20
  - pydantic-settings>=2.2
 
14
  hooks:
15
  - id: mypy
16
  files: ^src/
17
+ exclude: ^folder|^src/app.py
18
  additional_dependencies:
19
  - pydantic>=2.7
20
  - pydantic-settings>=2.2
dev/__init__.py CHANGED
@@ -1,2 +1 @@
1
  """Development utilities and plugins."""
2
-
 
1
  """Development utilities and plugins."""
 
docs/LICENSE.md CHANGED
@@ -32,3 +32,4 @@ SOFTWARE.
32
 
33
 
34
 
 
 
32
 
33
 
34
 
35
+
pyproject.toml CHANGED
@@ -127,6 +127,7 @@ ignore = [
127
  "PLR0913", # Too many arguments (agents need many params)
128
  "PLR0912", # Too many branches (complex orchestrator logic)
129
  "PLR0911", # Too many return statements (complex agent logic)
 
130
  "PLR2004", # Magic values (statistical constants like p-values)
131
  "PLW0603", # Global statement (singleton pattern for Modal)
132
  "PLC0415", # Lazy imports for optional dependencies
@@ -152,6 +153,7 @@ exclude = [
152
  "^reference_repos/",
153
  "^examples/",
154
  "^folder/",
 
155
  ]
156
 
157
  # ============== PYTEST CONFIG ==============
 
127
  "PLR0913", # Too many arguments (agents need many params)
128
  "PLR0912", # Too many branches (complex orchestrator logic)
129
  "PLR0911", # Too many return statements (complex agent logic)
130
+ "PLR0915", # Too many statements (Gradio UI setup functions)
131
  "PLR2004", # Magic values (statistical constants like p-values)
132
  "PLW0603", # Global statement (singleton pattern for Modal)
133
  "PLC0415", # Lazy imports for optional dependencies
 
153
  "^reference_repos/",
154
  "^examples/",
155
  "^folder/",
156
+ "^src/app.py",
157
  ]
158
 
159
  # ============== PYTEST CONFIG ==============
src/app.py CHANGED
@@ -583,7 +583,6 @@ async def research_agent(
583
  yield chat_msg
584
 
585
  # Optional: Generate audio output if enabled
586
- audio_output_data: tuple[int, np.ndarray[Any, Any]] | None = None # type: ignore[type-arg]
587
  if settings.enable_audio_output and settings.modal_available:
588
  try:
589
  from src.services.tts_modal import get_tts_service
@@ -592,7 +591,7 @@ async def research_agent(
592
  # Get the last message from history for TTS
593
  last_message = history[-1].get("content", "") if history else processed_text
594
  if last_message:
595
- audio_output_data = await tts_service.synthesize_async(
596
  text=last_message,
597
  voice=tts_voice,
598
  speed=tts_speed,
@@ -834,6 +833,48 @@ def create_demo() -> gr.Blocks:
834
  info="Select inference provider (leave empty for auto-select). Sign in to see all available providers.",
835
  )
836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  # Web Search Provider selection
838
  gr.Markdown("### 🔍 Web Search Provider")
839
 
@@ -1064,41 +1105,6 @@ def create_demo() -> gr.Blocks:
1064
  outputs=[tts_voice_dropdown, tts_speed_slider, audio_output],
1065
  )
1066
 
1067
- # Update model/provider dropdowns when user clicks refresh button
1068
- # Note: Gradio doesn't directly support watching OAuthToken/OAuthProfile changes
1069
- # So we provide a refresh button that users can click after logging in
1070
- def refresh_models_and_providers(
1071
- oauth_token: gr.OAuthToken | None = None,
1072
- oauth_profile: gr.OAuthProfile | None = None,
1073
- ) -> tuple[dict[str, Any], dict[str, Any], str]:
1074
- """Handle refresh button click and update dropdowns."""
1075
- import asyncio
1076
-
1077
- # Run async function in sync context
1078
- loop = asyncio.new_event_loop()
1079
- asyncio.set_event_loop(loop)
1080
- try:
1081
- result = loop.run_until_complete(
1082
- update_model_provider_dropdowns(oauth_token, oauth_profile)
1083
- )
1084
- return result
1085
- finally:
1086
- loop.close()
1087
-
1088
- refresh_models_btn = gr.Button(
1089
- value="🔄 Refresh Available Models",
1090
- visible=True,
1091
- size="sm",
1092
- )
1093
-
1094
- # Note: OAuthToken and OAuthProfile are automatically passed to functions
1095
- # when they are available in the Gradio context
1096
- refresh_models_btn.click(
1097
- fn=refresh_models_and_providers,
1098
- inputs=[], # OAuth components are automatically available in Gradio context
1099
- outputs=[hf_model_dropdown, hf_provider_dropdown, model_provider_status],
1100
- )
1101
-
1102
  # Chat interface with multimodal support
1103
  # Examples are provided but will NOT run at startup (cache_examples=False)
1104
  # Users must log in first before using examples or submitting queries
 
583
  yield chat_msg
584
 
585
  # Optional: Generate audio output if enabled
 
586
  if settings.enable_audio_output and settings.modal_available:
587
  try:
588
  from src.services.tts_modal import get_tts_service
 
591
  # Get the last message from history for TTS
592
  last_message = history[-1].get("content", "") if history else processed_text
593
  if last_message:
594
+ await tts_service.synthesize_async(
595
  text=last_message,
596
  voice=tts_voice,
597
  speed=tts_speed,
 
833
  info="Select inference provider (leave empty for auto-select). Sign in to see all available providers.",
834
  )
835
 
836
+ # Refresh button for updating models/providers after login
837
+ def refresh_models_and_providers(
838
+ request: gr.Request,
839
+ ) -> tuple[dict[str, Any], dict[str, Any], str]:
840
+ """Handle refresh button click and update dropdowns."""
841
+ import asyncio
842
+
843
+ # Extract OAuth token and profile from request
844
+ oauth_token: gr.OAuthToken | None = None
845
+ oauth_profile: gr.OAuthProfile | None = None
846
+
847
+ if request is not None:
848
+ # Try to get OAuth token from request
849
+ if hasattr(request, "oauth_token"):
850
+ oauth_token = request.oauth_token
851
+ if hasattr(request, "oauth_profile"):
852
+ oauth_profile = request.oauth_profile
853
+
854
+ # Run async function in sync context
855
+ loop = asyncio.new_event_loop()
856
+ asyncio.set_event_loop(loop)
857
+ try:
858
+ result = loop.run_until_complete(
859
+ update_model_provider_dropdowns(oauth_token, oauth_profile)
860
+ )
861
+ return result
862
+ finally:
863
+ loop.close()
864
+
865
+ refresh_models_btn = gr.Button(
866
+ value="🔄 Refresh Available Models",
867
+ visible=True,
868
+ size="sm",
869
+ )
870
+
871
+ # Pass request to get OAuth token from Gradio context
872
+ refresh_models_btn.click(
873
+ fn=refresh_models_and_providers,
874
+ inputs=[], # Request is automatically available in Gradio context
875
+ outputs=[hf_model_dropdown, hf_provider_dropdown, model_provider_status],
876
+ )
877
+
878
  # Web Search Provider selection
879
  gr.Markdown("### 🔍 Web Search Provider")
880
 
 
1105
  outputs=[tts_voice_dropdown, tts_speed_slider, audio_output],
1106
  )
1107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1108
  # Chat interface with multimodal support
1109
  # Examples are provided but will NOT run at startup (cache_examples=False)
1110
  # Users must log in first before using examples or submitting queries
src/orchestrator/graph_orchestrator.py CHANGED
@@ -886,10 +886,11 @@ class GraphOrchestrator:
886
  async def _execute_standard_agent(
887
  self, node: AgentNode, input_data: Any, query: str, context: GraphExecutionContext
888
  ) -> Any:
889
- """Execute standard agent with error handling."""
890
  # Get message history from context (limit to most recent 10 messages for token efficiency)
891
  message_history = context.get_message_history(max_messages=10)
892
 
 
893
  try:
894
  # Pass message_history if available (Pydantic AI agents support this)
895
  if message_history:
@@ -909,13 +910,204 @@ class GraphOrchestrator:
909
  "Failed to accumulate messages from agent result", error=str(e)
910
  )
911
  return result
912
- except Exception:
913
- # Handle validation errors and API errors for planner node
 
 
 
 
 
 
 
 
 
914
  if node.node_id == "planner":
 
 
 
 
 
 
 
 
 
 
 
 
 
915
  return self._create_fallback_plan(query, input_data)
916
- # For other nodes, re-raise the exception
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
  raise
918
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919
  def _create_fallback_plan(self, query: str, input_data: Any) -> Any:
920
  """Create fallback ReportPlan when planner fails."""
921
  from src.utils.models import ReportPlan, ReportPlanSection
 
886
  async def _execute_standard_agent(
887
  self, node: AgentNode, input_data: Any, query: str, context: GraphExecutionContext
888
  ) -> Any:
889
+ """Execute standard agent with error handling and fallback models."""
890
  # Get message history from context (limit to most recent 10 messages for token efficiency)
891
  message_history = context.get_message_history(max_messages=10)
892
 
893
+ # Try with the original agent first
894
  try:
895
  # Pass message_history if available (Pydantic AI agents support this)
896
  if message_history:
 
910
  "Failed to accumulate messages from agent result", error=str(e)
911
  )
912
  return result
913
+ except Exception as e:
914
+ # Check if we should retry with fallback models
915
+ from src.utils.hf_error_handler import (
916
+ extract_error_details,
917
+ should_retry_with_fallback,
918
+ )
919
+
920
+ error_details = extract_error_details(e)
921
+ should_retry = should_retry_with_fallback(e)
922
+
923
+ # Handle validation errors and API errors for planner node (with fallback)
924
  if node.node_id == "planner":
925
+ if should_retry:
926
+ self.logger.warning(
927
+ "Planner failed, trying fallback models",
928
+ original_error=str(e),
929
+ status_code=error_details.get("status_code"),
930
+ )
931
+ # Try fallback models for planner
932
+ fallback_result = await self._try_fallback_models(
933
+ node, input_data, message_history, query, context, e
934
+ )
935
+ if fallback_result is not None:
936
+ return fallback_result
937
+ # If fallback failed or not applicable, use fallback plan
938
  return self._create_fallback_plan(query, input_data)
939
+
940
+ # For other nodes, try fallback models if applicable
941
+ if should_retry:
942
+ self.logger.warning(
943
+ "Agent node failed, trying fallback models",
944
+ node_id=node.node_id,
945
+ original_error=str(e),
946
+ status_code=error_details.get("status_code"),
947
+ )
948
+ fallback_result = await self._try_fallback_models(
949
+ node, input_data, message_history, query, context, e
950
+ )
951
+ if fallback_result is not None:
952
+ return fallback_result
953
+
954
+ # If fallback didn't work or wasn't applicable, re-raise the exception
955
  raise
956
 
957
+ async def _try_fallback_models(
958
+ self,
959
+ node: AgentNode,
960
+ input_data: Any,
961
+ message_history: list[Any],
962
+ query: str,
963
+ context: GraphExecutionContext,
964
+ original_error: Exception,
965
+ ) -> Any | None:
966
+ """Try executing agent with fallback models.
967
+
968
+ Args:
969
+ node: The agent node that failed
970
+ input_data: Input data for the agent
971
+ message_history: Message history for the agent
972
+ query: The research query
973
+ context: Execution context
974
+ original_error: The original error that triggered fallback
975
+
976
+ Returns:
977
+ Agent result if successful, None if all fallbacks failed
978
+ """
979
+ from src.utils.hf_error_handler import extract_error_details, get_fallback_models
980
+
981
+ error_details = extract_error_details(original_error)
982
+ original_model = error_details.get("model_name")
983
+ fallback_models = get_fallback_models(original_model)
984
+
985
+ # Also try models from settings fallback list
986
+ from src.utils.config import settings
987
+
988
+ settings_fallbacks = settings.get_hf_fallback_models_list()
989
+ for model in settings_fallbacks:
990
+ if model not in fallback_models:
991
+ fallback_models.append(model)
992
+
993
+ self.logger.info(
994
+ "Trying fallback models",
995
+ node_id=node.node_id,
996
+ original_model=original_model,
997
+ fallback_count=len(fallback_models),
998
+ )
999
+
1000
+ # Try each fallback model
1001
+ for fallback_model in fallback_models:
1002
+ try:
1003
+ # Recreate agent with fallback model
1004
+ fallback_agent = self._recreate_agent_with_model(node.node_id, fallback_model)
1005
+ if fallback_agent is None:
1006
+ continue
1007
+
1008
+ # Try running with fallback agent
1009
+ if message_history:
1010
+ result = await fallback_agent.run(input_data, message_history=message_history)
1011
+ else:
1012
+ result = await fallback_agent.run(input_data)
1013
+
1014
+ self.logger.info(
1015
+ "Fallback model succeeded",
1016
+ node_id=node.node_id,
1017
+ fallback_model=fallback_model,
1018
+ )
1019
+
1020
+ # Accumulate new messages from agent result if available
1021
+ if hasattr(result, "new_messages"):
1022
+ try:
1023
+ new_messages = result.new_messages()
1024
+ for msg in new_messages:
1025
+ context.add_message(msg)
1026
+ except Exception as e:
1027
+ self.logger.debug(
1028
+ "Failed to accumulate messages from fallback agent result", error=str(e)
1029
+ )
1030
+
1031
+ return result
1032
+
1033
+ except Exception as e:
1034
+ self.logger.warning(
1035
+ "Fallback model failed",
1036
+ node_id=node.node_id,
1037
+ fallback_model=fallback_model,
1038
+ error=str(e),
1039
+ )
1040
+ continue
1041
+
1042
+ # All fallback models failed
1043
+ self.logger.error(
1044
+ "All fallback models failed",
1045
+ node_id=node.node_id,
1046
+ fallback_count=len(fallback_models),
1047
+ )
1048
+ return None
1049
+
1050
+ def _recreate_agent_with_model(self, node_id: str, model_name: str) -> Any | None:
1051
+ """Recreate an agent with a specific model.
1052
+
1053
+ Args:
1054
+ node_id: The node ID (e.g., "thinking", "knowledge_gap")
1055
+ model_name: The model name to use
1056
+
1057
+ Returns:
1058
+ Agent instance or None if recreation failed
1059
+ """
1060
+ try:
1061
+ from pydantic_ai.models.huggingface import HuggingFaceModel
1062
+ from pydantic_ai.providers.huggingface import HuggingFaceProvider
1063
+
1064
+ # Create model with fallback model name
1065
+ hf_provider = HuggingFaceProvider(api_key=self.oauth_token)
1066
+ model = HuggingFaceModel(model_name, provider=hf_provider)
1067
+
1068
+ # Recreate agent based on node_id
1069
+ if node_id == "thinking":
1070
+ from src.agent_factory.agents import create_thinking_agent
1071
+
1072
+ agent_wrapper = create_thinking_agent(model=model, oauth_token=self.oauth_token)
1073
+ return agent_wrapper.agent
1074
+ elif node_id == "knowledge_gap":
1075
+ from src.agent_factory.agents import create_knowledge_gap_agent
1076
+
1077
+ agent_wrapper = create_knowledge_gap_agent( # type: ignore[assignment]
1078
+ model=model, oauth_token=self.oauth_token
1079
+ )
1080
+ return agent_wrapper.agent
1081
+ elif node_id == "tool_selector":
1082
+ from src.agent_factory.agents import create_tool_selector_agent
1083
+
1084
+ agent_wrapper = create_tool_selector_agent( # type: ignore[assignment]
1085
+ model=model, oauth_token=self.oauth_token
1086
+ )
1087
+ return agent_wrapper.agent
1088
+ elif node_id == "planner":
1089
+ from src.agent_factory.agents import create_planner_agent
1090
+
1091
+ agent_wrapper = create_planner_agent(model=model, oauth_token=self.oauth_token) # type: ignore[assignment]
1092
+ return agent_wrapper.agent
1093
+ elif node_id == "writer":
1094
+ from src.agent_factory.agents import create_writer_agent
1095
+
1096
+ agent_wrapper = create_writer_agent(model=model, oauth_token=self.oauth_token) # type: ignore[assignment]
1097
+ return agent_wrapper.agent
1098
+ else:
1099
+ self.logger.warning("Unknown node_id for agent recreation", node_id=node_id)
1100
+ return None
1101
+
1102
+ except Exception as e:
1103
+ self.logger.error(
1104
+ "Failed to recreate agent with fallback model",
1105
+ node_id=node_id,
1106
+ model_name=model_name,
1107
+ error=str(e),
1108
+ )
1109
+ return None
1110
+
1111
  def _create_fallback_plan(self, query: str, input_data: Any) -> Any:
1112
  """Create fallback ReportPlan when planner fails."""
1113
  from src.utils.models import ReportPlan, ReportPlanSection
src/tools/vendored/searchxng_client.py CHANGED
@@ -94,4 +94,3 @@ class SearchXNGClient:
94
  except Exception as e:
95
  logger.error("Unexpected error in SearchXNG search", error=str(e), query=query)
96
  raise SearchError(f"SearchXNG search failed: {e}") from e
97
-
 
94
  except Exception as e:
95
  logger.error("Unexpected error in SearchXNG search", error=str(e), query=query)
96
  raise SearchError(f"SearchXNG search failed: {e}") from e
 
src/tools/vendored/serper_client.py CHANGED
@@ -90,4 +90,3 @@ class SerperClient:
90
  except Exception as e:
91
  logger.error("Unexpected error in Serper search", error=str(e), query=query)
92
  raise SearchError(f"Serper search failed: {e}") from e
93
-
 
90
  except Exception as e:
91
  logger.error("Unexpected error in Serper search", error=str(e), query=query)
92
  raise SearchError(f"Serper search failed: {e}") from e
 
src/tools/vendored/web_search_core.py CHANGED
@@ -199,4 +199,3 @@ def is_valid_url(url: str) -> bool:
199
  if any(ext in url for ext in restricted_extensions):
200
  return False
201
  return True
202
-
 
199
  if any(ext in url for ext in restricted_extensions):
200
  return False
201
  return True
 
src/utils/hf_error_handler.py CHANGED
@@ -197,4 +197,3 @@ def get_fallback_models(original_model: str | None = None) -> list[str]:
197
  fallbacks.remove(original_model)
198
 
199
  return fallbacks
200
-
 
197
  fallbacks.remove(original_model)
198
 
199
  return fallbacks
 
src/utils/markdown.css CHANGED
@@ -21,3 +21,4 @@ body {
21
 
22
 
23
 
 
 
21
 
22
 
23
 
24
+
src/utils/md_to_pdf.py CHANGED
@@ -61,4 +61,3 @@ def md_to_pdf(md_text: str, pdf_file_path: str) -> None:
61
  md2pdf(pdf_file_path, md_text, css_file_path=str(css_path))
62
 
63
  logger.debug("PDF generated successfully", pdf_path=pdf_file_path)
64
-
 
61
  md2pdf(pdf_file_path, md_text, css_file_path=str(css_path))
62
 
63
  logger.debug("PDF generated successfully", pdf_path=pdf_file_path)
 
tests/unit/middleware/test_budget_tracker_phase7.py CHANGED
@@ -167,3 +167,4 @@ class TestIterationTokenTracking:
167
 
168
 
169
 
 
 
167
 
168
 
169
 
170
+
tests/unit/middleware/test_workflow_manager.py CHANGED
@@ -293,3 +293,4 @@ class TestWorkflowManager:
293
 
294
 
295
 
 
 
293
 
294
 
295
 
296
+
tests/unit/utils/test_hf_error_handler.py CHANGED
@@ -236,3 +236,4 @@ class TestGetFallbackModels:
236
 
237
 
238
 
 
 
236
 
237
 
238
 
239
+
tests/unit/utils/test_hf_model_validator.py CHANGED
@@ -413,3 +413,4 @@ class TestValidateOAuthToken:
413
 
414
 
415
 
 
 
413
 
414
 
415
 
416
+