Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Upload folder using huggingface_hub
Browse files- README.md +6 -3
- app.py +11 -3
- apps/app_enhanced.py +159 -56
- core/lightweight_a2a_manager.py +293 -0
- core/simulated_a2a_manager.py +136 -0
- requirements_hf.txt +22 -0
README.md
CHANGED
@@ -20,7 +20,7 @@ Experience AI agents with distinct personalities competing in a fantasy football
|
|
20 |
- **6 AI Agents** with unique strategies (Zero RB, Best Player Available, Robust RB, etc.)
|
21 |
- **Two Communication Modes**:
|
22 |
- **Basic Multiagent**: Fast, single-process execution
|
23 |
-
- **A2A Mode**: Distributed agents
|
24 |
- **Interactive Participation**: Draft alongside AI with strategic advice
|
25 |
- **Real-time Communication**: Agents comment and react to picks
|
26 |
- **Multi-User Support**: Each user gets their own draft session
|
@@ -32,8 +32,11 @@ Experience AI agents with distinct personalities competing in a fantasy football
|
|
32 |
|
33 |
## About Communication Modes
|
34 |
|
35 |
-
- **Basic Multiagent (Recommended)**: Works perfectly
|
36 |
-
- **A2A Mode (
|
|
|
|
|
|
|
37 |
|
38 |
## About
|
39 |
|
|
|
20 |
- **6 AI Agents** with unique strategies (Zero RB, Best Player Available, Robust RB, etc.)
|
21 |
- **Two Communication Modes**:
|
22 |
- **Basic Multiagent**: Fast, single-process execution
|
23 |
+
- **A2A Mode**: Distributed agents with automatic fallback (Full β Lightweight β Simulated)
|
24 |
- **Interactive Participation**: Draft alongside AI with strategic advice
|
25 |
- **Real-time Communication**: Agents comment and react to picks
|
26 |
- **Multi-User Support**: Each user gets their own draft session
|
|
|
32 |
|
33 |
## About Communication Modes
|
34 |
|
35 |
+
- **Basic Multiagent (Recommended)**: Works perfectly everywhere! All agents run in a single process with fast, reliable communication.
|
36 |
+
- **A2A Mode (Adaptive)**: Automatically selects the best distributed mode:
|
37 |
+
- **Full A2A**: Complete protocol with gRPC (when available)
|
38 |
+
- **Lightweight A2A**: HTTP-only servers (works on HF Spaces!)
|
39 |
+
- **Simulated A2A**: Mock distributed experience (fallback)
|
40 |
|
41 |
## About
|
42 |
|
app.py
CHANGED
@@ -17,10 +17,18 @@ if os.getenv("SPACE_ID"):
|
|
17 |
# Test the specific imports that fail
|
18 |
import a2a # This is what usually fails
|
19 |
from any_agent.serving import A2AServingConfig
|
20 |
-
print("β
Surprisingly, A2A dependencies are available! You can try A2A mode.")
|
21 |
except ImportError as e:
|
22 |
-
print(f"β οΈ A2A dependencies not available: {e}")
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
# Import and run the enhanced app
|
26 |
from apps.app_enhanced import main
|
|
|
17 |
# Test the specific imports that fail
|
18 |
import a2a # This is what usually fails
|
19 |
from any_agent.serving import A2AServingConfig
|
20 |
+
print("β
Surprisingly, full A2A dependencies are available! You can try A2A mode.")
|
21 |
except ImportError as e:
|
22 |
+
print(f"β οΈ Full A2A dependencies not available: {e}")
|
23 |
+
# Check if lightweight A2A can work
|
24 |
+
try:
|
25 |
+
import httpx
|
26 |
+
import fastapi
|
27 |
+
import uvicorn
|
28 |
+
print("β
Lightweight A2A dependencies available! A2A mode will work using HTTP-only.")
|
29 |
+
os.environ["A2A_MODE"] = "lightweight"
|
30 |
+
except ImportError:
|
31 |
+
print("β
No problem! Basic Multiagent mode works perfectly and is recommended.")
|
32 |
|
33 |
# Import and run the enhanced app
|
34 |
from apps.app_enhanced import main
|
apps/app_enhanced.py
CHANGED
@@ -76,10 +76,28 @@ class EnhancedFantasyDraftApp:
|
|
76 |
try:
|
77 |
global DynamicA2AAgentManager, cleanup_session
|
78 |
from core.dynamic_a2a_manager import DynamicA2AAgentManager, cleanup_session
|
|
|
|
|
79 |
except ImportError as e:
|
80 |
-
|
81 |
-
|
82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
# Generate unique session ID if needed
|
85 |
if not self.session_id:
|
@@ -92,7 +110,15 @@ class EnhancedFantasyDraftApp:
|
|
92 |
try:
|
93 |
await self.a2a_manager.start_agents()
|
94 |
ports = self.a2a_manager.allocated_ports
|
95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
except RuntimeError as e:
|
97 |
# Failed to allocate ports or start agents
|
98 |
self.a2a_status = f"β Failed to start A2A: {str(e)}"
|
@@ -146,9 +172,18 @@ class EnhancedFantasyDraftApp:
|
|
146 |
self.draft_output = "# π Mock Draft with A2A Communication\n\n"
|
147 |
|
148 |
# Welcome message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
149 |
self.draft_output += format_agent_message(
|
150 |
"commissioner", "ALL",
|
151 |
-
|
152 |
)
|
153 |
yield self.draft_output
|
154 |
|
@@ -560,10 +595,13 @@ def create_gradio_interface():
|
|
560 |
)
|
561 |
mode_info = gr.Markdown(
|
562 |
"""
|
563 |
-
**Basic Multiagent** (Recommended
|
564 |
-
**A2A**: Distributed agents with
|
|
|
|
|
|
|
565 |
|
566 |
-
*
|
567 |
"""
|
568 |
)
|
569 |
|
@@ -856,74 +894,139 @@ def create_gradio_interface():
|
|
856 |
def test_a2a_functionality():
|
857 |
"""Test A2A dependencies and port availability."""
|
858 |
import socket
|
|
|
|
|
|
|
|
|
859 |
test_results = []
|
860 |
|
861 |
-
#
|
862 |
-
test_results.append("===
|
|
|
|
|
|
|
|
|
|
|
|
|
863 |
try:
|
864 |
-
|
865 |
-
|
866 |
-
|
867 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
868 |
try:
|
869 |
-
import a2a
|
870 |
-
test_results.append("β
a2a
|
871 |
try:
|
872 |
-
|
873 |
-
test_results.append("β
a2a.types
|
874 |
except ImportError as e:
|
875 |
-
test_results.append(f"β
|
876 |
-
except ImportError:
|
877 |
-
test_results.append("β a2a module NOT found - this is why A2A fails!")
|
878 |
-
test_results.append(" The a2a-sdk package should provide the 'a2a' module")
|
879 |
-
|
880 |
-
try:
|
881 |
-
from any_agent.serving import A2AServingConfig
|
882 |
-
from any_agent.tools import a2a_tool_async
|
883 |
-
test_results.append("β
A2A components imported successfully!")
|
884 |
except ImportError as e:
|
885 |
-
test_results.append(f"β
|
886 |
except ImportError as e:
|
887 |
-
test_results.append(f"β
|
888 |
|
889 |
-
#
|
890 |
-
|
891 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
892 |
available_count = 0
|
893 |
|
894 |
for port in test_ports:
|
895 |
try:
|
896 |
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
897 |
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
898 |
-
|
899 |
-
|
900 |
-
|
901 |
-
for addr in ['127.0.0.1', 'localhost', '0.0.0.0']:
|
902 |
-
try:
|
903 |
-
sock.bind((addr, port))
|
904 |
-
test_results.append(f"β
Port {port} available on {addr}")
|
905 |
-
bound = True
|
906 |
-
available_count += 1
|
907 |
-
break
|
908 |
-
except:
|
909 |
-
continue
|
910 |
-
|
911 |
-
if not bound:
|
912 |
-
test_results.append(f"β Port {port} not available")
|
913 |
-
|
914 |
sock.close()
|
915 |
-
except Exception
|
916 |
-
test_results.append(f"β Port {port}
|
917 |
|
918 |
test_results.append(f"\nπ Summary: {available_count}/{len(test_ports)} ports available")
|
919 |
|
920 |
-
#
|
921 |
-
|
922 |
-
|
923 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
924 |
|
925 |
-
|
926 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
927 |
else:
|
928 |
test_results.append("\nβ Not enough ports available. Use Basic Multiagent mode.")
|
929 |
|
|
|
76 |
try:
|
77 |
global DynamicA2AAgentManager, cleanup_session
|
78 |
from core.dynamic_a2a_manager import DynamicA2AAgentManager, cleanup_session
|
79 |
+
self.real_a2a = True
|
80 |
+
self.a2a_type = "full"
|
81 |
except ImportError as e:
|
82 |
+
# Try lightweight A2A for HF Spaces (no grpcio)
|
83 |
+
try:
|
84 |
+
from core.lightweight_a2a_manager import LightweightA2AAgentManager, cleanup_session
|
85 |
+
DynamicA2AAgentManager = LightweightA2AAgentManager
|
86 |
+
self.real_a2a = True
|
87 |
+
self.a2a_type = "lightweight"
|
88 |
+
print("Using lightweight A2A mode (HTTP-only, no grpcio)")
|
89 |
+
except ImportError as e2:
|
90 |
+
# Fall back to simulated A2A
|
91 |
+
try:
|
92 |
+
from core.simulated_a2a_manager import SimulatedA2AAgentManager, cleanup_session
|
93 |
+
DynamicA2AAgentManager = SimulatedA2AAgentManager
|
94 |
+
self.real_a2a = False
|
95 |
+
self.a2a_type = "simulated"
|
96 |
+
print("Using simulated A2A mode (real A2A not available)")
|
97 |
+
except ImportError as e3:
|
98 |
+
self.a2a_status = f"β A2A mode not available: {str(e)}. Please use Basic Multiagent mode."
|
99 |
+
self.use_real_a2a = False
|
100 |
+
return self.a2a_status
|
101 |
|
102 |
# Generate unique session ID if needed
|
103 |
if not self.session_id:
|
|
|
110 |
try:
|
111 |
await self.a2a_manager.start_agents()
|
112 |
ports = self.a2a_manager.allocated_ports
|
113 |
+
if hasattr(self, 'a2a_type'):
|
114 |
+
if self.a2a_type == "full":
|
115 |
+
self.a2a_status = f"β
Full A2A Mode Active (Session: {self.session_id}, Ports: {ports[0]}-{ports[-1]})"
|
116 |
+
elif self.a2a_type == "lightweight":
|
117 |
+
self.a2a_status = f"β
Lightweight A2A Mode Active (Session: {self.session_id}, HTTP Ports: {ports[0]}-{ports[-1]})"
|
118 |
+
else: # simulated
|
119 |
+
self.a2a_status = f"β
Simulated A2A Mode Active (Session: {self.session_id}, Mock Ports: {ports[0]}-{ports[-1]})"
|
120 |
+
else:
|
121 |
+
self.a2a_status = f"β
A2A Mode Active (Session: {self.session_id}, Ports: {ports[0]}-{ports[-1]})"
|
122 |
except RuntimeError as e:
|
123 |
# Failed to allocate ports or start agents
|
124 |
self.a2a_status = f"β Failed to start A2A: {str(e)}"
|
|
|
172 |
self.draft_output = "# π Mock Draft with A2A Communication\n\n"
|
173 |
|
174 |
# Welcome message
|
175 |
+
if hasattr(self, 'a2a_type'):
|
176 |
+
if self.a2a_type == "full":
|
177 |
+
welcome_msg = "Welcome to the A2A-powered draft! Each agent is running on its own server with full A2A protocol."
|
178 |
+
elif self.a2a_type == "lightweight":
|
179 |
+
welcome_msg = "Welcome to the lightweight A2A draft! Each agent runs on its own HTTP server (no grpcio needed)."
|
180 |
+
else: # simulated
|
181 |
+
welcome_msg = "Welcome to the simulated A2A draft! Agents communicate using mock HTTP calls."
|
182 |
+
else:
|
183 |
+
welcome_msg = "Welcome to the A2A-powered draft! Each agent is running on its own server."
|
184 |
self.draft_output += format_agent_message(
|
185 |
"commissioner", "ALL",
|
186 |
+
welcome_msg
|
187 |
)
|
188 |
yield self.draft_output
|
189 |
|
|
|
595 |
)
|
596 |
mode_info = gr.Markdown(
|
597 |
"""
|
598 |
+
**Basic Multiagent** (Recommended): Fast, single-process execution (β
Multi-user safe)
|
599 |
+
**A2A**: Distributed agents mode with automatic fallback:
|
600 |
+
- Full A2A: gRPC + HTTP protocol (requires grpcio)
|
601 |
+
- Lightweight A2A: HTTP-only (works on HF Spaces!)
|
602 |
+
- Simulated A2A: Mock HTTP calls (fallback)
|
603 |
|
604 |
+
*A2A mode will automatically select the best available option.*
|
605 |
"""
|
606 |
)
|
607 |
|
|
|
894 |
def test_a2a_functionality():
|
895 |
"""Test A2A dependencies and port availability."""
|
896 |
import socket
|
897 |
+
import subprocess
|
898 |
+
import importlib.util
|
899 |
+
import site
|
900 |
+
|
901 |
test_results = []
|
902 |
|
903 |
+
# 1. Python Environment
|
904 |
+
test_results.append("=== Python Environment ===")
|
905 |
+
test_results.append(f"Python: {sys.version.split()[0]}")
|
906 |
+
test_results.append(f"Platform: {sys.platform}")
|
907 |
+
test_results.append(f"SPACE_ID: {os.getenv('SPACE_ID', 'Not on HF Spaces')}")
|
908 |
+
|
909 |
+
# 2. Check a2a-sdk installation
|
910 |
+
test_results.append("\n=== Package Installation ===")
|
911 |
try:
|
912 |
+
result = subprocess.run([sys.executable, "-m", "pip", "show", "a2a-sdk"],
|
913 |
+
capture_output=True, text=True, timeout=5)
|
914 |
+
if result.returncode == 0:
|
915 |
+
version_line = [line for line in result.stdout.split('\n') if line.startswith('Version:')]
|
916 |
+
location_line = [line for line in result.stdout.split('\n') if line.startswith('Location:')]
|
917 |
+
test_results.append(f"β
a2a-sdk installed: {version_line[0] if version_line else 'Unknown version'}")
|
918 |
+
if location_line:
|
919 |
+
test_results.append(f" {location_line[0]}")
|
920 |
+
else:
|
921 |
+
test_results.append("β a2a-sdk NOT installed according to pip")
|
922 |
+
except Exception as e:
|
923 |
+
test_results.append(f"β Error checking pip: {e}")
|
924 |
+
|
925 |
+
# 3. Module search
|
926 |
+
test_results.append("\n=== Module Search ===")
|
927 |
+
a2a_spec = importlib.util.find_spec("a2a")
|
928 |
+
if a2a_spec:
|
929 |
+
test_results.append(f"β
a2a module found at: {a2a_spec.origin}")
|
930 |
+
else:
|
931 |
+
test_results.append("β a2a module NOT found by importlib")
|
932 |
+
# Manual search
|
933 |
+
for path in site.getsitepackages():
|
934 |
+
if os.path.exists(path):
|
935 |
+
a2a_path = os.path.join(path, "a2a")
|
936 |
+
if os.path.exists(a2a_path):
|
937 |
+
test_results.append(f" Found a2a directory at: {a2a_path}")
|
938 |
+
|
939 |
+
# 4. Import tests
|
940 |
+
test_results.append("\n=== Import Tests ===")
|
941 |
+
|
942 |
+
# Basic a2a import
|
943 |
+
try:
|
944 |
+
import a2a
|
945 |
+
test_results.append(f"β
import a2a: Success")
|
946 |
try:
|
947 |
+
import a2a.types
|
948 |
+
test_results.append("β
import a2a.types: Success")
|
949 |
try:
|
950 |
+
from a2a.types import AgentSkill
|
951 |
+
test_results.append("β
from a2a.types import AgentSkill: Success")
|
952 |
except ImportError as e:
|
953 |
+
test_results.append(f"β AgentSkill import: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
954 |
except ImportError as e:
|
955 |
+
test_results.append(f"β a2a.types import: {e}")
|
956 |
except ImportError as e:
|
957 |
+
test_results.append(f"β a2a import failed: {e}")
|
958 |
|
959 |
+
# any_agent A2A imports
|
960 |
+
try:
|
961 |
+
from any_agent.serving import A2AServingConfig
|
962 |
+
from any_agent.tools import a2a_tool_async
|
963 |
+
test_results.append("β
any_agent A2A components: Success!")
|
964 |
+
except ImportError as e:
|
965 |
+
test_results.append(f"β any_agent A2A import: {e}")
|
966 |
+
|
967 |
+
# 5. Port availability
|
968 |
+
test_results.append("\n=== Port Availability ===")
|
969 |
+
test_ports = [5001, 5002, 5003, 5004, 5005, 5006]
|
970 |
available_count = 0
|
971 |
|
972 |
for port in test_ports:
|
973 |
try:
|
974 |
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
975 |
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
976 |
+
sock.bind(('127.0.0.1', port))
|
977 |
+
test_results.append(f"β
Port {port} available")
|
978 |
+
available_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
979 |
sock.close()
|
980 |
+
except Exception:
|
981 |
+
test_results.append(f"β Port {port} not available")
|
982 |
|
983 |
test_results.append(f"\nπ Summary: {available_count}/{len(test_ports)} ports available")
|
984 |
|
985 |
+
# 6. Try fixing a2a if needed
|
986 |
+
if "β a2a import failed" in "\n".join(test_results):
|
987 |
+
test_results.append("\n=== Attempting Fix ===")
|
988 |
+
try:
|
989 |
+
# Try reinstalling without deps
|
990 |
+
result = subprocess.run(
|
991 |
+
[sys.executable, "-m", "pip", "install", "a2a-sdk", "--no-deps", "--force-reinstall"],
|
992 |
+
capture_output=True, text=True, timeout=10
|
993 |
+
)
|
994 |
+
if result.returncode == 0:
|
995 |
+
test_results.append("β
Reinstalled a2a-sdk")
|
996 |
+
# Test import again
|
997 |
+
try:
|
998 |
+
import importlib
|
999 |
+
if 'a2a' in sys.modules:
|
1000 |
+
del sys.modules['a2a']
|
1001 |
+
import a2a
|
1002 |
+
test_results.append("β
Import after reinstall: Success!")
|
1003 |
+
except Exception as e:
|
1004 |
+
test_results.append(f"β Import after reinstall: {e}")
|
1005 |
+
else:
|
1006 |
+
test_results.append(f"β Reinstall failed: {result.stderr[:200]}")
|
1007 |
+
except Exception as e:
|
1008 |
+
test_results.append(f"β Fix attempt error: {e}")
|
1009 |
|
1010 |
+
# Test lightweight A2A requirements
|
1011 |
+
test_results.append("\n=== Lightweight A2A Test ===")
|
1012 |
+
try:
|
1013 |
+
import httpx
|
1014 |
+
import fastapi
|
1015 |
+
import uvicorn
|
1016 |
+
test_results.append("β
HTTP dependencies available (httpx, fastapi, uvicorn)")
|
1017 |
+
lightweight_ready = True
|
1018 |
+
except ImportError as e:
|
1019 |
+
test_results.append(f"β Missing HTTP dependencies: {e}")
|
1020 |
+
lightweight_ready = False
|
1021 |
+
|
1022 |
+
# Final verdict
|
1023 |
+
if available_count >= 6:
|
1024 |
+
if "β
any_agent A2A components: Success!" in "\n".join(test_results):
|
1025 |
+
test_results.append("\nβ
Full A2A available! A2A mode will use the complete protocol.")
|
1026 |
+
elif lightweight_ready:
|
1027 |
+
test_results.append("\nβ
Lightweight A2A available! A2A mode will use HTTP-only servers.")
|
1028 |
+
else:
|
1029 |
+
test_results.append("\nβ οΈ Only Simulated A2A available. A2A mode will use mock communication.")
|
1030 |
else:
|
1031 |
test_results.append("\nβ Not enough ports available. Use Basic Multiagent mode.")
|
1032 |
|
core/lightweight_a2a_manager.py
ADDED
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Lightweight A2A Manager for HF Spaces - Real distributed agents using HTTP only.
|
3 |
+
Works without grpcio dependencies by using httpx and FastAPI directly.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import asyncio
|
7 |
+
import httpx
|
8 |
+
import uvicorn
|
9 |
+
from fastapi import FastAPI, HTTPException
|
10 |
+
from pydantic import BaseModel
|
11 |
+
from typing import Optional, List, Dict, Any
|
12 |
+
import multiprocessing
|
13 |
+
import time
|
14 |
+
import socket
|
15 |
+
from contextlib import closing
|
16 |
+
from core.agent import FantasyDraftAgent
|
17 |
+
from core.constants import AGENT_CONFIGS
|
18 |
+
|
19 |
+
|
20 |
+
# Pydantic models for API
|
21 |
+
class PickRequest(BaseModel):
|
22 |
+
available_players: List[str]
|
23 |
+
previous_picks: List[str]
|
24 |
+
round_num: int
|
25 |
+
|
26 |
+
|
27 |
+
class PickResponse(BaseModel):
|
28 |
+
type: str = "pick"
|
29 |
+
player_name: str
|
30 |
+
reasoning: str
|
31 |
+
trash_talk: Optional[str] = None
|
32 |
+
|
33 |
+
|
34 |
+
class CommentRequest(BaseModel):
|
35 |
+
picking_team: int
|
36 |
+
player: str
|
37 |
+
round_num: int
|
38 |
+
|
39 |
+
|
40 |
+
class CommentResponse(BaseModel):
|
41 |
+
comment: Optional[str]
|
42 |
+
|
43 |
+
|
44 |
+
class LightweightA2AAgent:
|
45 |
+
"""Single agent server that runs in its own process"""
|
46 |
+
|
47 |
+
def __init__(self, team_num: int, port: int):
|
48 |
+
self.team_num = team_num
|
49 |
+
self.port = port
|
50 |
+
self.app = FastAPI()
|
51 |
+
self.agent = None
|
52 |
+
self._setup_routes()
|
53 |
+
|
54 |
+
def _setup_routes(self):
|
55 |
+
@self.app.on_event("startup")
|
56 |
+
async def startup():
|
57 |
+
config = AGENT_CONFIGS[self.team_num]
|
58 |
+
self.agent = FantasyDraftAgent(
|
59 |
+
team_name=config["team_name"],
|
60 |
+
strategy=config["strategy"],
|
61 |
+
traits=config["traits"],
|
62 |
+
rival_teams=config.get("rival_teams", [])
|
63 |
+
)
|
64 |
+
print(f"β
Agent {self.team_num} initialized on port {self.port}")
|
65 |
+
|
66 |
+
@self.app.get("/health")
|
67 |
+
async def health():
|
68 |
+
return {"status": "healthy", "team": self.team_num}
|
69 |
+
|
70 |
+
@self.app.post("/pick", response_model=PickResponse)
|
71 |
+
async def make_pick(request: PickRequest):
|
72 |
+
if not self.agent:
|
73 |
+
raise HTTPException(status_code=500, detail="Agent not initialized")
|
74 |
+
|
75 |
+
# Update agent's picks
|
76 |
+
self.agent.picks = request.previous_picks.copy()
|
77 |
+
|
78 |
+
# Make pick
|
79 |
+
player = self.agent.make_pick(request.available_players, request.round_num)
|
80 |
+
reasoning = self.agent.explain_pick(player, request.round_num)
|
81 |
+
|
82 |
+
# Optional trash talk
|
83 |
+
trash_talk = None
|
84 |
+
import random
|
85 |
+
if random.random() < 0.3:
|
86 |
+
trash_talk = random.choice([
|
87 |
+
f"Can't believe {player} was still available!",
|
88 |
+
f"{player} is going to be huge this year!",
|
89 |
+
"Building a championship team here.",
|
90 |
+
"Y'all sleeping on my picks!"
|
91 |
+
])
|
92 |
+
|
93 |
+
return PickResponse(
|
94 |
+
player_name=player,
|
95 |
+
reasoning=reasoning,
|
96 |
+
trash_talk=trash_talk
|
97 |
+
)
|
98 |
+
|
99 |
+
@self.app.post("/comment", response_model=CommentResponse)
|
100 |
+
async def make_comment(request: CommentRequest):
|
101 |
+
if not self.agent:
|
102 |
+
raise HTTPException(status_code=500, detail="Agent not initialized")
|
103 |
+
|
104 |
+
# Higher chance of comment for rivals
|
105 |
+
import random
|
106 |
+
if request.picking_team in self.agent.rival_teams:
|
107 |
+
if random.random() < 0.8:
|
108 |
+
comment = self.agent.react_to_pick(
|
109 |
+
f"Team {request.picking_team}",
|
110 |
+
request.player,
|
111 |
+
request.round_num
|
112 |
+
)
|
113 |
+
return CommentResponse(comment=comment)
|
114 |
+
else:
|
115 |
+
if random.random() < 0.3:
|
116 |
+
comment = self.agent.react_to_pick(
|
117 |
+
f"Team {request.picking_team}",
|
118 |
+
request.player,
|
119 |
+
request.round_num
|
120 |
+
)
|
121 |
+
return CommentResponse(comment=comment)
|
122 |
+
|
123 |
+
return CommentResponse(comment=None)
|
124 |
+
|
125 |
+
def run(self):
|
126 |
+
"""Run the agent server"""
|
127 |
+
uvicorn.run(self.app, host="127.0.0.1", port=self.port, log_level="error")
|
128 |
+
|
129 |
+
|
130 |
+
def _run_agent_server(team_num: int, port: int):
|
131 |
+
"""Function to run in separate process"""
|
132 |
+
agent = LightweightA2AAgent(team_num, port)
|
133 |
+
agent.run()
|
134 |
+
|
135 |
+
|
136 |
+
class LightweightA2AAgentManager:
|
137 |
+
"""
|
138 |
+
Manages lightweight A2A agents using only HTTP (no grpcio).
|
139 |
+
Each agent runs as a FastAPI server in a separate process.
|
140 |
+
"""
|
141 |
+
|
142 |
+
def __init__(self, session_id: str = "lightweight"):
|
143 |
+
self.session_id = session_id
|
144 |
+
self.processes: Dict[int, multiprocessing.Process] = {}
|
145 |
+
self.allocated_ports: List[int] = []
|
146 |
+
self.base_port = 5001
|
147 |
+
self.running = False
|
148 |
+
self.max_comments_per_pick = 2
|
149 |
+
self.client = None
|
150 |
+
|
151 |
+
def _find_free_port(self, start_port: int) -> int:
|
152 |
+
"""Find a free port starting from start_port"""
|
153 |
+
for port in range(start_port, start_port + 100):
|
154 |
+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
155 |
+
try:
|
156 |
+
sock.bind(('127.0.0.1', port))
|
157 |
+
return port
|
158 |
+
except OSError:
|
159 |
+
continue
|
160 |
+
raise RuntimeError(f"No free ports found starting from {start_port}")
|
161 |
+
|
162 |
+
async def start_agents(self):
|
163 |
+
"""Start all agent servers"""
|
164 |
+
print(f"π Starting lightweight A2A agents for session {self.session_id}...")
|
165 |
+
|
166 |
+
# Initialize HTTP client
|
167 |
+
self.client = httpx.AsyncClient(timeout=10.0)
|
168 |
+
|
169 |
+
# Allocate ports and start processes
|
170 |
+
for team_num in [1, 2, 3, 5, 6]:
|
171 |
+
port = self._find_free_port(self.base_port + team_num - 1)
|
172 |
+
self.allocated_ports.append(port)
|
173 |
+
|
174 |
+
# Start agent process
|
175 |
+
process = multiprocessing.Process(
|
176 |
+
target=_run_agent_server,
|
177 |
+
args=(team_num, port)
|
178 |
+
)
|
179 |
+
process.start()
|
180 |
+
self.processes[team_num] = process
|
181 |
+
|
182 |
+
print(f"β³ Starting agent {team_num} on port {port}...")
|
183 |
+
|
184 |
+
# Wait for all agents to be ready
|
185 |
+
await self._wait_for_agents()
|
186 |
+
|
187 |
+
self.running = True
|
188 |
+
print(f"β
All lightweight A2A agents ready!")
|
189 |
+
|
190 |
+
async def _wait_for_agents(self):
|
191 |
+
"""Wait for all agents to respond to health checks"""
|
192 |
+
max_retries = 30
|
193 |
+
for team_num, port in zip([1, 2, 3, 5, 6], self.allocated_ports):
|
194 |
+
url = f"http://127.0.0.1:{port}/health"
|
195 |
+
for i in range(max_retries):
|
196 |
+
try:
|
197 |
+
response = await self.client.get(url)
|
198 |
+
if response.status_code == 200:
|
199 |
+
print(f"β
Agent {team_num} ready on port {port}")
|
200 |
+
break
|
201 |
+
except:
|
202 |
+
pass
|
203 |
+
await asyncio.sleep(0.5)
|
204 |
+
else:
|
205 |
+
raise RuntimeError(f"Agent {team_num} failed to start on port {port}")
|
206 |
+
|
207 |
+
async def get_pick(self, team_num: int, available_players: List[str],
|
208 |
+
previous_picks: List[str], round_num: int) -> Optional[PickResponse]:
|
209 |
+
"""Get pick from agent via HTTP"""
|
210 |
+
if team_num not in self.processes:
|
211 |
+
return None
|
212 |
+
|
213 |
+
port_index = [1, 2, 3, 5, 6].index(team_num)
|
214 |
+
port = self.allocated_ports[port_index]
|
215 |
+
|
216 |
+
try:
|
217 |
+
response = await self.client.post(
|
218 |
+
f"http://127.0.0.1:{port}/pick",
|
219 |
+
json={
|
220 |
+
"available_players": available_players,
|
221 |
+
"previous_picks": previous_picks,
|
222 |
+
"round_num": round_num
|
223 |
+
}
|
224 |
+
)
|
225 |
+
|
226 |
+
if response.status_code == 200:
|
227 |
+
data = response.json()
|
228 |
+
return PickResponse(**data)
|
229 |
+
else:
|
230 |
+
print(f"Error from agent {team_num}: {response.status_code}")
|
231 |
+
return None
|
232 |
+
except Exception as e:
|
233 |
+
print(f"Failed to get pick from agent {team_num}: {e}")
|
234 |
+
return None
|
235 |
+
|
236 |
+
async def get_comment(self, commenting_team: int, picking_team: int,
|
237 |
+
player: str, round_num: int) -> Optional[str]:
|
238 |
+
"""Get comment from agent via HTTP"""
|
239 |
+
if commenting_team not in self.processes:
|
240 |
+
return None
|
241 |
+
|
242 |
+
port_index = [1, 2, 3, 5, 6].index(commenting_team)
|
243 |
+
port = self.allocated_ports[port_index]
|
244 |
+
|
245 |
+
try:
|
246 |
+
response = await self.client.post(
|
247 |
+
f"http://127.0.0.1:{port}/comment",
|
248 |
+
json={
|
249 |
+
"picking_team": picking_team,
|
250 |
+
"player": player,
|
251 |
+
"round_num": round_num
|
252 |
+
}
|
253 |
+
)
|
254 |
+
|
255 |
+
if response.status_code == 200:
|
256 |
+
data = response.json()
|
257 |
+
return data.get("comment")
|
258 |
+
else:
|
259 |
+
return None
|
260 |
+
except Exception as e:
|
261 |
+
print(f"Failed to get comment from agent {commenting_team}: {e}")
|
262 |
+
return None
|
263 |
+
|
264 |
+
async def cleanup(self):
|
265 |
+
"""Stop all agent servers"""
|
266 |
+
if self.running:
|
267 |
+
print(f"π Stopping lightweight A2A agents for session {self.session_id}...")
|
268 |
+
|
269 |
+
# Close HTTP client
|
270 |
+
if self.client:
|
271 |
+
await self.client.aclose()
|
272 |
+
|
273 |
+
# Terminate all processes
|
274 |
+
for team_num, process in self.processes.items():
|
275 |
+
if process.is_alive():
|
276 |
+
process.terminate()
|
277 |
+
process.join(timeout=2)
|
278 |
+
if process.is_alive():
|
279 |
+
process.kill()
|
280 |
+
process.join()
|
281 |
+
print(f"β
Agent {team_num} stopped")
|
282 |
+
|
283 |
+
self.processes.clear()
|
284 |
+
self.allocated_ports.clear()
|
285 |
+
self.running = False
|
286 |
+
print(f"β
Lightweight A2A session {self.session_id} cleaned up")
|
287 |
+
|
288 |
+
|
289 |
+
# Provide same cleanup function interface
|
290 |
+
async def cleanup_session(manager: LightweightA2AAgentManager):
|
291 |
+
"""Clean up a lightweight A2A session"""
|
292 |
+
if manager:
|
293 |
+
await manager.cleanup()
|
core/simulated_a2a_manager.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Simulated A2A Manager for environments where real A2A won't work (like HF Spaces).
|
3 |
+
Provides the same interface and experience as real A2A but uses in-process communication.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import asyncio
|
7 |
+
import time
|
8 |
+
import random
|
9 |
+
from typing import Optional, List, Dict
|
10 |
+
from core.agent import FantasyDraftAgent
|
11 |
+
from core.constants import AGENT_CONFIGS
|
12 |
+
|
13 |
+
|
14 |
+
class SimulatedA2AResponse:
|
15 |
+
"""Simulated response that looks like an A2A response"""
|
16 |
+
def __init__(self, player_name: str, reasoning: str, trash_talk: Optional[str] = None):
|
17 |
+
self.type = "pick"
|
18 |
+
self.player_name = player_name
|
19 |
+
self.reasoning = reasoning
|
20 |
+
self.trash_talk = trash_talk
|
21 |
+
|
22 |
+
|
23 |
+
class SimulatedA2AAgentManager:
|
24 |
+
"""
|
25 |
+
Simulates A2A behavior without actual HTTP servers.
|
26 |
+
Provides the same interface as DynamicA2AAgentManager but runs in-process.
|
27 |
+
"""
|
28 |
+
|
29 |
+
def __init__(self, session_id: str = "sim"):
|
30 |
+
self.session_id = session_id
|
31 |
+
self.agents: Dict[int, FantasyDraftAgent] = {}
|
32 |
+
self.running = False
|
33 |
+
self.max_comments_per_pick = 2
|
34 |
+
# Simulate port allocation
|
35 |
+
self.allocated_ports = [5001, 5002, 5003, 5004, 5005, 5006]
|
36 |
+
|
37 |
+
async def start_agents(self):
|
38 |
+
"""Initialize agents (simulated startup)"""
|
39 |
+
print(f"π Starting simulated A2A agents for session {self.session_id}...")
|
40 |
+
|
41 |
+
# Simulate startup delay
|
42 |
+
await asyncio.sleep(0.5)
|
43 |
+
|
44 |
+
# Create agents
|
45 |
+
for team_num in [1, 2, 3, 5, 6]:
|
46 |
+
config = AGENT_CONFIGS[team_num]
|
47 |
+
self.agents[team_num] = FantasyDraftAgent(
|
48 |
+
team_name=config["team_name"],
|
49 |
+
strategy=config["strategy"],
|
50 |
+
traits=config["traits"],
|
51 |
+
rival_teams=config.get("rival_teams", [])
|
52 |
+
)
|
53 |
+
|
54 |
+
# Simulate server startup messages
|
55 |
+
for team_num, port in zip([1, 2, 3, 5, 6], self.allocated_ports):
|
56 |
+
await asyncio.sleep(0.1)
|
57 |
+
print(f"β
Agent {team_num} ready on simulated port {port}")
|
58 |
+
|
59 |
+
self.running = True
|
60 |
+
print(f"β
All simulated A2A agents ready!")
|
61 |
+
|
62 |
+
async def get_pick(self, team_num: int, available_players: List[str],
|
63 |
+
previous_picks: List[str], round_num: int) -> Optional[SimulatedA2AResponse]:
|
64 |
+
"""Get pick from agent (simulated A2A call)"""
|
65 |
+
if team_num not in self.agents:
|
66 |
+
return None
|
67 |
+
|
68 |
+
# Simulate network delay
|
69 |
+
await asyncio.sleep(random.uniform(0.2, 0.5))
|
70 |
+
|
71 |
+
agent = self.agents[team_num]
|
72 |
+
|
73 |
+
# Get pick decision
|
74 |
+
player = agent.make_pick(available_players, round_num)
|
75 |
+
|
76 |
+
# Get reasoning
|
77 |
+
reasoning = agent.explain_pick(player, round_num)
|
78 |
+
|
79 |
+
# Maybe add trash talk
|
80 |
+
trash_talk = None
|
81 |
+
if random.random() < 0.3: # 30% chance of trash talk
|
82 |
+
responses = [
|
83 |
+
f"Easy choice. Can't believe {player} was still available!",
|
84 |
+
f"You all sleeping? {player} is a steal here!",
|
85 |
+
f"Building a championship team, one pick at a time.",
|
86 |
+
"This is how you draft, take notes everyone."
|
87 |
+
]
|
88 |
+
trash_talk = random.choice(responses)
|
89 |
+
|
90 |
+
return SimulatedA2AResponse(player, reasoning, trash_talk)
|
91 |
+
|
92 |
+
async def get_comment(self, commenting_team: int, picking_team: int,
|
93 |
+
player: str, round_num: int) -> Optional[str]:
|
94 |
+
"""Get comment from another agent (simulated A2A call)"""
|
95 |
+
if commenting_team not in self.agents:
|
96 |
+
return None
|
97 |
+
|
98 |
+
# Simulate network delay
|
99 |
+
await asyncio.sleep(random.uniform(0.1, 0.3))
|
100 |
+
|
101 |
+
agent = self.agents[commenting_team]
|
102 |
+
|
103 |
+
# Higher chance of comment if they're rivals
|
104 |
+
if picking_team in agent.rival_teams:
|
105 |
+
if random.random() < 0.8: # 80% chance for rivals
|
106 |
+
return agent.react_to_pick(
|
107 |
+
self.agents[picking_team].team_name,
|
108 |
+
player,
|
109 |
+
round_num
|
110 |
+
)
|
111 |
+
else:
|
112 |
+
if random.random() < 0.3: # 30% chance for non-rivals
|
113 |
+
return agent.react_to_pick(
|
114 |
+
self.agents[picking_team].team_name,
|
115 |
+
player,
|
116 |
+
round_num
|
117 |
+
)
|
118 |
+
|
119 |
+
return None
|
120 |
+
|
121 |
+
async def cleanup(self):
|
122 |
+
"""Cleanup simulated agents"""
|
123 |
+
if self.running:
|
124 |
+
print(f"π Stopping simulated A2A agents for session {self.session_id}...")
|
125 |
+
# Simulate shutdown
|
126 |
+
await asyncio.sleep(0.2)
|
127 |
+
self.agents.clear()
|
128 |
+
self.running = False
|
129 |
+
print(f"β
Simulated A2A session {self.session_id} cleaned up")
|
130 |
+
|
131 |
+
|
132 |
+
# Provide same cleanup function interface
|
133 |
+
async def cleanup_session(manager: SimulatedA2AAgentManager):
|
134 |
+
"""Clean up a simulated A2A session"""
|
135 |
+
if manager:
|
136 |
+
await manager.cleanup()
|
requirements_hf.txt
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Special requirements for Hugging Face Spaces
|
2 |
+
# Avoids grpcio dependencies that won't install properly
|
3 |
+
|
4 |
+
# Install any-agent WITHOUT a2a extras
|
5 |
+
any-agent[openai]>=0.21.0
|
6 |
+
|
7 |
+
# Direct HTTP dependencies
|
8 |
+
httpx>=0.24.0
|
9 |
+
fastapi>=0.100.0
|
10 |
+
uvicorn>=0.22.0
|
11 |
+
sse-starlette>=1.6.0
|
12 |
+
|
13 |
+
# App dependencies
|
14 |
+
python-dotenv
|
15 |
+
pydantic>=2.0.0
|
16 |
+
gradio>=4.0.0
|
17 |
+
nest-asyncio
|
18 |
+
aiohttp
|
19 |
+
typing-extensions
|
20 |
+
|
21 |
+
# Note: We'll use httpx directly for A2A-like communication
|
22 |
+
# without the heavyweight grpcio dependencies
|