Skip to content

Commit df52248

Browse files
author
William Garcia
committed
refactor(gateway): simplify tools to accept direct inputs and switch agent models to Nova
Tools now accept explicit position/player data instead of full game_state. Agent models updated: DEF/FWD2 → nova-lite, FWD1/GK → nova-micro, MID → nova-pro. BedrockModel now uses defaults instead of fixed temperature/max_tokens.
1 parent 259c446 commit df52248

10 files changed

Lines changed: 142 additions & 189 deletions

File tree

agentic-football-sample-agents/ai-team-strands-gateway/ai-def/src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
fallback_commands = build_fallback(DEF_CONFIG)
4949

5050
agent, mcp_client = create_gateway_agent(
51-
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.anthropic.claude-3-5-haiku-20241022-v1:0"
51+
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.amazon.nova-lite-v1:0"
5252
)
5353
create_gateway_invoke_handler(
5454
app, agent, mcp_client, MY_PLAYER_ID, POSITION_LABEL, fallback_commands,

agentic-football-sample-agents/ai-team-strands-gateway/ai-fwd1/src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
fallback_commands = build_fallback(FWD1_CONFIG)
5050

5151
agent, mcp_client = create_gateway_agent(
52-
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.anthropic.claude-3-5-haiku-20241022-v1:0"
52+
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.amazon.nova-micro-v1:0"
5353
)
5454
create_gateway_invoke_handler(
5555
app, agent, mcp_client, MY_PLAYER_ID, POSITION_LABEL, fallback_commands,

agentic-football-sample-agents/ai-team-strands-gateway/ai-fwd2/src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
fallback_commands = build_fallback(FWD2_CONFIG)
5050

5151
agent, mcp_client = create_gateway_agent(
52-
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.anthropic.claude-3-5-haiku-20241022-v1:0"
52+
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.amazon.nova-lite-v1:0"
5353
)
5454
create_gateway_invoke_handler(
5555
app, agent, mcp_client, MY_PLAYER_ID, POSITION_LABEL, fallback_commands,

agentic-football-sample-agents/ai-team-strands-gateway/ai-gk/src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
fallback_commands = build_fallback(GK_CONFIG)
4848

4949
agent, mcp_client = create_gateway_agent(
50-
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.anthropic.claude-3-5-haiku-20241022-v1:0"
50+
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.amazon.nova-micro-v1:0"
5151
)
5252
create_gateway_invoke_handler(
5353
app, agent, mcp_client, MY_PLAYER_ID, POSITION_LABEL, fallback_commands,

agentic-football-sample-agents/ai-team-strands-gateway/ai-mid/src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
fallback_commands = build_fallback(MID_CONFIG)
5050

5151
agent, mcp_client = create_gateway_agent(
52-
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.anthropic.claude-3-5-haiku-20241022-v1:0"
52+
SYSTEM_PROMPT, MY_PLAYER_ID, POSITION_LABEL, model_id="us.amazon.nova-pro-v1:0"
5353
)
5454
create_gateway_invoke_handler(
5555
app, agent, mcp_client, MY_PLAYER_ID, POSITION_LABEL, fallback_commands,

agentic-football-sample-agents/ai-team-strands-gateway/gateway_agent_base.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,7 @@ def create_gateway_agent(
4949
when invoking the agent so tools remain available.
5050
"""
5151
mcp_client = MCPClient(_create_gateway_transport)
52-
model = BedrockModel(
53-
model_id=model_id,
54-
temperature=0,
55-
max_tokens=5000,
56-
)
52+
model = BedrockModel(model_id=model_id)
5753

5854
# Fetch tool definitions inside the context so the connection is active.
5955
with mcp_client:
Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,58 @@
11
"""Gateway Tool: calculate_pass_options
22
3-
Accepts game_state + team_id + player_id from the MCP schema,
4-
extracts the relevant data, and calculates pass success probability
5-
for each teammate based on distance and interception risk.
3+
Given a player's position, teammates, and opponents, calculates pass
4+
success probability for each teammate based on distance and interception risk.
5+
6+
Deployed as a Lambda behind AgentCore Gateway.
67
"""
78

89
import json
910
import math
1011

1112

12-
def _distance(p1, p2):
13+
def _distance(p1: dict, p2: dict) -> float:
1314
return math.sqrt((p1["x"] - p2["x"]) ** 2 + (p1["y"] - p2["y"]) ** 2)
1415

1516

16-
def _player_idx(p):
17-
if "agentId" in p:
18-
try:
19-
return int(p["agentId"].rsplit("_", 1)[-1])
20-
except (ValueError, IndexError):
21-
return 0
22-
return p.get("playerId", 0)
23-
24-
25-
def _is_team(p, team_id):
26-
if "teamCode" in p:
27-
return p["teamCode"] == ("home" if team_id == 0 else "away")
28-
return p.get("teamId") == team_id
29-
30-
31-
def _interception_risk(passer, receiver, opponents):
17+
def _interception_risk(passer: dict, receiver: dict, opponents: list[dict]) -> float:
18+
"""Estimate interception probability (0-1) based on opponents near the pass lane."""
3219
pass_dist = _distance(passer, receiver)
3320
if pass_dist < 1:
3421
return 0.0
22+
3523
dx = receiver["x"] - passer["x"]
3624
dy = receiver["y"] - passer["y"]
3725
risk = 0.0
26+
3827
for opp in opponents:
28+
# Project opponent onto pass vector
3929
ox = opp["x"] - passer["x"]
4030
oy = opp["y"] - passer["y"]
4131
t = max(0, min(1, (ox * dx + oy * dy) / (pass_dist ** 2)))
42-
cx = passer["x"] + t * dx
43-
cy = passer["y"] + t * dy
44-
d = math.sqrt((opp["x"] - cx) ** 2 + (opp["y"] - cy) ** 2)
45-
if d < 8:
46-
risk = max(risk, 1.0 - (d / 8.0))
32+
closest_x = passer["x"] + t * dx
33+
closest_y = passer["y"] + t * dy
34+
dist_to_lane = math.sqrt((opp["x"] - closest_x) ** 2 + (opp["y"] - closest_y) ** 2)
35+
36+
if dist_to_lane < 8:
37+
risk = max(risk, 1.0 - (dist_to_lane / 8.0))
38+
4739
return min(risk, 0.95)
4840

4941

5042
def lambda_handler(event, context):
43+
"""Lambda handler — input: passer position, teammates, opponents."""
5144
body = json.loads(event.get("body", "{}")) if isinstance(event.get("body"), str) else event
5245

53-
game_state = body.get("game_state", body)
54-
team_id = body.get("team_id", 0)
55-
player_id = body.get("player_id", 0)
56-
57-
players = game_state.get("players", [])
58-
me = next((p for p in players if _player_idx(p) == player_id and _is_team(p, team_id)), None)
59-
passer_pos = me["position"] if me else {"x": 0, "y": 0}
60-
61-
teammates = [
62-
{"player_id": _player_idx(p), "position": p["position"]}
63-
for p in players if _is_team(p, team_id) and _player_idx(p) != player_id
64-
]
65-
opp_positions = [p["position"] for p in players if not _is_team(p, team_id)]
46+
passer_pos = body["passer_position"]
47+
teammates = body["teammates"]
48+
opponents = body["opponents"]
6649

6750
options = []
6851
for tm in teammates:
6952
dist = round(_distance(passer_pos, tm["position"]), 1)
70-
risk = _interception_risk(passer_pos, tm["position"], opp_positions)
53+
risk = _interception_risk(passer_pos, tm["position"], [o["position"] for o in opponents])
7154
success = round(max(0.05, 1.0 - risk - (dist / 120.0)), 2)
55+
7256
options.append({
7357
"player_id": tm["player_id"],
7458
"distance": dist,
@@ -78,4 +62,8 @@ def lambda_handler(event, context):
7862
})
7963

8064
options.sort(key=lambda x: x["success_probability"], reverse=True)
81-
return {"statusCode": 200, "body": json.dumps({"pass_options": options})}
65+
66+
return {
67+
"statusCode": 200,
68+
"body": json.dumps({"pass_options": options}),
69+
}
Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,85 @@
11
"""Gateway Tool: evaluate_shot
22
3-
Accepts game_state + team_id + player_id from the MCP schema,
4-
extracts shooter/GK positions, and evaluates shot success probability.
3+
Evaluates shot success probability from a given position,
4+
considering distance to goal, angle, and goalkeeper position.
5+
6+
Deployed as a Lambda behind AgentCore Gateway.
57
"""
68

79
import json
810
import math
911

10-
GOAL_HOME = {"x": -55, "y": 0}
11-
GOAL_AWAY = {"x": 55, "y": 0}
12-
GOAL_HALF_WIDTH = 5.0
12+
# Goal center positions
13+
GOAL_HOME = {"x": -55, "y": 0} # HOME team's goal
14+
GOAL_AWAY = {"x": 55, "y": 0} # AWAY team's goal
15+
GOAL_HALF_WIDTH = 5.0 # approximate half-width of goal
1316

1417

15-
def _distance(p1, p2):
18+
def _distance(p1: dict, p2: dict) -> float:
1619
return math.sqrt((p1["x"] - p2["x"]) ** 2 + (p1["y"] - p2["y"]) ** 2)
1720

1821

19-
def _player_idx(p):
20-
if "agentId" in p:
21-
try:
22-
return int(p["agentId"].rsplit("_", 1)[-1])
23-
except (ValueError, IndexError):
24-
return 0
25-
return p.get("playerId", 0)
26-
27-
28-
def _is_team(p, team_id):
29-
if "teamCode" in p:
30-
return p["teamCode"] == ("home" if team_id == 0 else "away")
31-
return p.get("teamId") == team_id
32-
33-
3422
def lambda_handler(event, context):
23+
"""Lambda handler — input: shooter position, GK position, team side."""
3524
body = json.loads(event.get("body", "{}")) if isinstance(event.get("body"), str) else event
3625

37-
game_state = body.get("game_state", body)
38-
team_id = body.get("team_id", 0)
39-
player_id = body.get("player_id", 0)
40-
41-
players = game_state.get("players", [])
42-
me = next((p for p in players if _player_idx(p) == player_id and _is_team(p, team_id)), None)
43-
shooter = me["position"] if me else {"x": 0, "y": 0}
26+
shooter = body["shooter_position"]
27+
gk_pos = body["goalkeeper_position"]
28+
team_side = body.get("team_side", "HOME")
29+
blockers = body.get("blocker_positions", [])
4430

45-
# Opponent GK is player index 0 on the other team
46-
opp_gk = next((p for p in players if _player_idx(p) == 0 and not _is_team(p, team_id)), None)
47-
gk_pos = opp_gk["position"] if opp_gk else {"x": 50, "y": 0}
48-
49-
team_side = "HOME" if team_id == 0 else "AWAY"
31+
# Target goal (opponent's goal)
5032
goal = GOAL_AWAY if team_side == "HOME" else GOAL_HOME
5133

52-
# Blockers: opponents near the shooting lane (excluding GK)
53-
blockers = [p["position"] for p in players if not _is_team(p, team_id) and _player_idx(p) != 0]
54-
5534
dist_to_goal = _distance(shooter, goal)
5635
dist_gk_to_goal = _distance(gk_pos, goal)
5736

37+
# Angle factor: wider angle = better chance
38+
dy = abs(shooter["y"])
5839
angle_to_goal = math.atan2(GOAL_HALF_WIDTH, dist_to_goal)
5940
angle_factor = min(1.0, angle_to_goal / 0.15)
41+
42+
# Distance factor: closer = better
6043
distance_factor = max(0.0, 1.0 - (dist_to_goal / 55.0))
44+
45+
# GK positioning factor: GK off-center = better chance
6146
gk_offset = abs(gk_pos["y"])
6247
gk_factor = min(1.0, gk_offset / 8.0) * 0.3
48+
49+
# GK distance factor: GK far from goal = better
6350
gk_dist_factor = min(1.0, dist_gk_to_goal / 15.0) * 0.2
6451

52+
# Blocker penalty
6553
blocker_penalty = 0.0
6654
for b in blockers:
6755
b_dist = _distance(shooter, b)
6856
if b_dist < 10:
6957
blocker_penalty += (10 - b_dist) / 10.0 * 0.15
7058
blocker_penalty = min(blocker_penalty, 0.4)
7159

72-
probability = round(max(0.02, min(0.95,
73-
(distance_factor * 0.45) + (angle_factor * 0.25) + gk_factor + gk_dist_factor - blocker_penalty
74-
)), 2)
60+
probability = round(
61+
max(0.02, min(0.95,
62+
(distance_factor * 0.45) + (angle_factor * 0.25) + gk_factor + gk_dist_factor - blocker_penalty
63+
)), 2
64+
)
7565

66+
# Recommend aim location based on GK position
7667
if gk_pos["y"] > 1:
7768
aim = "BL" if shooter["y"] > 0 else "BR"
7869
elif gk_pos["y"] < -1:
7970
aim = "TL" if shooter["y"] > 0 else "TR"
8071
else:
8172
aim = "TR" if shooter["y"] <= 0 else "TL"
8273

83-
return {"statusCode": 200, "body": json.dumps({
84-
"success_probability": probability,
85-
"distance_to_goal": round(dist_to_goal, 1),
86-
"recommended_aim": aim,
87-
"recommended_power": round(min(1.0, 0.6 + (dist_to_goal / 80.0)), 2),
88-
"should_shoot": probability > 0.25,
89-
})}
74+
recommended_power = round(min(1.0, 0.6 + (dist_to_goal / 80.0)), 2)
75+
76+
return {
77+
"statusCode": 200,
78+
"body": json.dumps({
79+
"success_probability": probability,
80+
"distance_to_goal": round(dist_to_goal, 1),
81+
"recommended_aim": aim,
82+
"recommended_power": recommended_power,
83+
"should_shoot": probability > 0.25,
84+
}),
85+
}

0 commit comments

Comments
 (0)