jolyonbrown Claude commited on
Commit
ab0da91
·
1 Parent(s): 8182f9c

Add Steam API integration and agentic workflow

Browse files

- Create CLAUDE.md with project context and hackathon requirements
- Implement Steam library fetch tool in tools/steam.py
- Add agentic loop to app.py with tool execution
- Update .gitignore to exclude .env files and Python artifacts
- Agent autonomously fetches Steam data and analyzes gaming patterns

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (4) hide show
  1. .gitignore +50 -0
  2. CLAUDE.md +158 -0
  3. app.py +106 -12
  4. tools/steam.py +140 -0
.gitignore ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ devdocs/
2
+
3
+ # Environment variables (contains API keys)
4
+ .env
5
+ .env.local
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ *$py.class
11
+ *.so
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+
29
+ # Virtual environments
30
+ venv/
31
+ ENV/
32
+ env/
33
+
34
+ # IDE
35
+ .vscode/
36
+ .idea/
37
+ *.swp
38
+ *.swo
39
+ *~
40
+
41
+ # OS
42
+ .DS_Store
43
+ Thumbs.db
44
+
45
+ # Testing
46
+ test_agent.py
47
+ *.log
48
+
49
+ # Gradio
50
+ flagged/
CLAUDE.md ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dream Game Forge - Project Instructions
2
+
3
+ ## Project Overview
4
+
5
+ Dream Game Forge is an agentic Gradio application for the MCP 1st Birthday Hackathon that analyzes a user's Steam gaming library to discover their ideal game, then generates a playable HTML5 prototype.
6
+
7
+ **CRITICAL:** Deadline is November 30, 2025, 11:59 PM UTC (TODAY). Prioritize working features over perfection.
8
+
9
+ ## Core Workflow
10
+
11
+ 1. User provides Steam ID
12
+ 2. Agent fetches Steam library + playtime data
13
+ 3. Analyzes gaming patterns (what they play vs. what they buy and ignore)
14
+ 4. Cross-references with IGDB for genre/theme data
15
+ 5. Identifies a unique game "gap" tailored to the user
16
+ 6. Generates a mini Game Design Document
17
+ 7. Produces a playable HTML5 Canvas game (single-file, no dependencies)
18
+
19
+ ## Hackathon Requirements
20
+
21
+ - **Track:** MCP in Action - Creative
22
+ - **Must include:**
23
+ - Gradio app interface
24
+ - MCP servers as tools (Steam API, IGDB API)
25
+ - Autonomous agent behavior with planning and reasoning
26
+ - **README tag:** `mcp-in-action-track-creative`
27
+ - **HF Space:** https://huggingface.co/spaces/MCP-1st-Birthday/DreamGameForge
28
+
29
+ ## Architecture Constraints
30
+
31
+ ### Why NOT Claude Agent SDK
32
+ The Agent SDK requires global npm install which HuggingFace Spaces don't support. Use raw Anthropic API with manual tool loop instead.
33
+
34
+ ### MCP Implementation
35
+ Build simple MCP-style servers in `tools/` directory. We satisfy the MCP requirement by structuring tools properly, even though we call them via Anthropic API tool interface.
36
+
37
+ ### Game Generation
38
+ Claude generates complete HTML5 Canvas code directly - no external game engines. Keep games simple (puzzle, clicker, basic platformer) but playable. Display via `gradio-iframe` component or downloadable HTML file.
39
+
40
+ ## File Structure
41
+
42
+ ```
43
+ DreamGameForge/
44
+ ├── app.py # Gradio app + agent loop
45
+ ├── tools/
46
+ │ ├── steam.py # Steam API functions
47
+ │ └── igdb.py # IGDB API functions
48
+ ├── agent.py # Agentic loop logic
49
+ ├── requirements.txt
50
+ └── README.md # Must include mcp-in-action-track-creative tag
51
+ ```
52
+
53
+ ## API Configuration
54
+
55
+ ### Required Secrets (set in HF Space settings)
56
+ - `ANTHROPIC_API_KEY`
57
+ - `STEAM_API_KEY`
58
+ - `TWITCH_CLIENT_ID` (for IGDB)
59
+ - `TWITCH_CLIENT_SECRET` (for IGDB)
60
+
61
+ ### Steam Web API
62
+ ```
63
+ Endpoint: https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/
64
+ Parameters: key, steamid, include_appinfo=true, include_played_free_games=true
65
+ Returns: List of games with appid, name, playtime_forever, playtime_2weeks
66
+ ```
67
+ Works for public profiles without OAuth.
68
+
69
+ ### IGDB API
70
+ ```
71
+ Endpoint: https://api.igdb.com/v4/games
72
+ Headers: Client-ID, Authorization: Bearer {token}
73
+ Body: Apicalypse query syntax
74
+ ```
75
+
76
+ Get access token from:
77
+ ```
78
+ POST https://id.twitch.tv/oauth2/token
79
+ Body: client_id, client_secret, grant_type=client_credentials
80
+ ```
81
+
82
+ Example queries:
83
+ - Search: `search "Hollow Knight"; fields name,genres.name,themes.name,summary; limit 10;`
84
+ - Filter: `where genres.name = "Platform" & rating > 80; fields name,summary; limit 20;`
85
+
86
+ ## System Prompt Philosophy
87
+
88
+ The agent should be decisive and autonomous:
89
+ 1. INVESTIGATE: Fetch Steam library, analyze playtime patterns
90
+ 2. ANALYZE: Cross-reference with IGDB for genres/themes/mechanics
91
+ 3. IDENTIFY THE GAP: What game should exist for this person?
92
+ 4. DESIGN: Write mini Game Design Document
93
+ 5. BUILD: Generate playable HTML5 Canvas prototype
94
+
95
+ Don't ask for permission - show reasoning and act.
96
+
97
+ ## Model Selection
98
+
99
+ Use `claude-sonnet-4-20250514` for the agent loop - balance of quality and speed.
100
+
101
+ ## Development Workflow
102
+
103
+ ### Local Testing
104
+ ```bash
105
+ pip install gradio anthropic httpx python-dotenv
106
+ export ANTHROPIC_API_KEY=...
107
+ export STEAM_API_KEY=...
108
+ export TWITCH_CLIENT_ID=...
109
+ export TWITCH_CLIENT_SECRET=...
110
+ python app.py
111
+ ```
112
+
113
+ ### Deployment
114
+ ```bash
115
+ git add .
116
+ git commit -m "descriptive message"
117
+ git push # Pushes to HuggingFace Space automatically
118
+ ```
119
+
120
+ ## Priority Order (Time-Constrained)
121
+
122
+ 1. ✅ Basic Gradio chat with Claude working
123
+ 2. Steam library fetch tool
124
+ 3. IGDB game details tool
125
+ 4. Agent loop with tools integrated
126
+ 5. System prompt refinement
127
+ 6. Game HTML generation capability
128
+ 7. Game display in Gradio (iframe or download)
129
+ 8. Polish, README, demo video, social post
130
+
131
+ ## Quick Wins If Time Runs Out
132
+
133
+ - Skip IGDB, use only Steam data + Claude's knowledge
134
+ - Serve game as downloadable file instead of embedded iframe
135
+ - Simpler game format (text adventure vs. canvas game)
136
+ - Pre-record demo video even if live version is flaky
137
+
138
+ ## Code Style
139
+
140
+ - Keep functions simple and focused
141
+ - Use type hints where practical
142
+ - Error handling for API calls (Steam/IGDB may fail)
143
+ - Log agent reasoning steps for debugging
144
+
145
+ ## Testing
146
+
147
+ - Test with a real Steam ID (public profile required)
148
+ - Verify API keys are working before full agent run
149
+ - Test generated HTML games in browser before deployment
150
+ - Check HF Space logs if deployment fails
151
+
152
+ ## Reference Links
153
+
154
+ - Space: https://huggingface.co/spaces/MCP-1st-Birthday/DreamGameForge
155
+ - Hackathon: https://huggingface.co/MCP-1st-Birthday
156
+ - Steam API Docs: https://developer.valvesoftware.com/wiki/Steam_Web_API
157
+ - IGDB API Docs: https://api-docs.igdb.com/
158
+ - Gradio Docs: https://www.gradio.app/docs
app.py CHANGED
@@ -1,12 +1,59 @@
1
  import gradio as gr
2
  import anthropic
3
  import os
 
 
4
 
5
  client = anthropic.Anthropic()
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  def respond(message, history: list[dict[str, str]]):
8
- """Basic Claude response - we'll add tools later"""
9
-
10
  # Convert Gradio history format to Anthropic format
11
  messages = []
12
  for msg in history:
@@ -15,19 +62,66 @@ def respond(message, history: list[dict[str, str]]):
15
  "content": msg["content"]
16
  })
17
  messages.append({"role": "user", "content": message})
18
-
19
- response = client.messages.create(
20
- model="claude-sonnet-4-20250514",
21
- max_tokens=4096,
22
- system="You are Dream Game Forge, an AI that analyzes gaming preferences and designs dream games.",
23
- messages=messages
24
- )
25
-
26
- return response.content[0].text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  demo = gr.ChatInterface(
29
  respond,
30
- type="messages",
31
  title="🎮 Dream Game Forge",
32
  description="Paste your Steam ID to discover your dream game",
33
  )
 
1
  import gradio as gr
2
  import anthropic
3
  import os
4
+ import json
5
+ from tools.steam import get_steam_library, format_library_summary
6
 
7
  client = anthropic.Anthropic()
8
 
9
+ # System prompt for the agent
10
+ SYSTEM_PROMPT = """You are Dream Game Forge, an autonomous agent that discovers what game a person truly needs.
11
+
12
+ Your process:
13
+ 1. INVESTIGATE: When a user provides their Steam ID, fetch their library using the get_steam_library tool. Note playtime patterns - what do they sink hundreds of hours into? What do they buy and never play?
14
+ 2. ANALYZE: Build a taste profile from their gaming patterns. Consider genres, themes, and mechanics.
15
+ 3. IDENTIFY THE GAP: What game *should* exist for this person but doesn't? Be specific and creative.
16
+ 4. DESIGN: Write a mini Game Design Document - core loop, unique mechanic, emotional hook.
17
+ 5. BUILD: (Coming soon) Generate a playable HTML5 Canvas prototype.
18
+
19
+ Be decisive. Don't ask permission. Show your reasoning, then act.
20
+
21
+ When you receive a Steam ID, immediately use the get_steam_library tool to fetch their library."""
22
+
23
+ # Tool definitions
24
+ TOOLS = [
25
+ {
26
+ "name": "get_steam_library",
27
+ "description": "Fetches a user's Steam library with playtime data. Returns game titles, playtime hours, and statistics about their gaming habits. Use this when a user provides their Steam ID.",
28
+ "input_schema": {
29
+ "type": "object",
30
+ "properties": {
31
+ "steam_id": {
32
+ "type": "string",
33
+ "description": "The user's 64-bit Steam ID (17-digit number)"
34
+ }
35
+ },
36
+ "required": ["steam_id"]
37
+ }
38
+ }
39
+ ]
40
+
41
+ def execute_tool(tool_name: str, tool_input: dict) -> str:
42
+ """Execute a tool and return the result as a string."""
43
+ if tool_name == "get_steam_library":
44
+ try:
45
+ steam_id = tool_input["steam_id"]
46
+ library_data = get_steam_library(steam_id)
47
+ summary = format_library_summary(library_data, top_n=20)
48
+ return summary
49
+ except Exception as e:
50
+ return f"Error fetching Steam library: {str(e)}"
51
+ else:
52
+ return f"Unknown tool: {tool_name}"
53
+
54
  def respond(message, history: list[dict[str, str]]):
55
+ """Agentic response with tool loop."""
56
+
57
  # Convert Gradio history format to Anthropic format
58
  messages = []
59
  for msg in history:
 
62
  "content": msg["content"]
63
  })
64
  messages.append({"role": "user", "content": message})
65
+
66
+ # Agentic loop
67
+ response_text = ""
68
+ max_iterations = 10
69
+
70
+ for iteration in range(max_iterations):
71
+ response = client.messages.create(
72
+ model="claude-sonnet-4-20250514",
73
+ max_tokens=4096,
74
+ system=SYSTEM_PROMPT,
75
+ tools=TOOLS,
76
+ messages=messages
77
+ )
78
+
79
+ # Collect text responses
80
+ for block in response.content:
81
+ if block.type == "text":
82
+ response_text += block.text
83
+
84
+ # Check if we need to execute tools
85
+ if response.stop_reason == "tool_use":
86
+ # Execute all tool calls
87
+ tool_results = []
88
+
89
+ for block in response.content:
90
+ if block.type == "tool_use":
91
+ tool_name = block.name
92
+ tool_input = block.input
93
+ tool_id = block.id
94
+
95
+ # Execute the tool
96
+ result = execute_tool(tool_name, tool_input)
97
+
98
+ tool_results.append({
99
+ "type": "tool_result",
100
+ "tool_use_id": tool_id,
101
+ "content": result
102
+ })
103
+
104
+ # Add assistant response and tool results to messages
105
+ messages.append({
106
+ "role": "assistant",
107
+ "content": response.content
108
+ })
109
+ messages.append({
110
+ "role": "user",
111
+ "content": tool_results
112
+ })
113
+
114
+ elif response.stop_reason == "end_turn":
115
+ # Done - return the accumulated response
116
+ break
117
+ else:
118
+ # Unexpected stop reason
119
+ break
120
+
121
+ return response_text
122
 
123
  demo = gr.ChatInterface(
124
  respond,
 
125
  title="🎮 Dream Game Forge",
126
  description="Paste your Steam ID to discover your dream game",
127
  )
tools/steam.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Steam Web API integration for Dream Game Forge."""
2
+
3
+ import os
4
+ import httpx
5
+ from typing import Dict, List, Any, Optional
6
+
7
+
8
+ def get_steam_library(steam_id: str) -> Dict[str, Any]:
9
+ """
10
+ Fetch a user's Steam library with playtime data.
11
+
12
+ Args:
13
+ steam_id: Steam ID (64-bit Steam ID)
14
+
15
+ Returns:
16
+ Dictionary containing game library data with playtime information
17
+
18
+ Raises:
19
+ ValueError: If STEAM_API_KEY is not set or API call fails
20
+ """
21
+ api_key = os.getenv("STEAM_API_KEY")
22
+ if not api_key:
23
+ raise ValueError("STEAM_API_KEY environment variable not set")
24
+
25
+ url = "https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/"
26
+ params = {
27
+ "key": api_key,
28
+ "steamid": steam_id,
29
+ "include_appinfo": "true",
30
+ "include_played_free_games": "true",
31
+ "format": "json"
32
+ }
33
+
34
+ try:
35
+ response = httpx.get(url, params=params, timeout=30.0)
36
+ response.raise_for_status()
37
+ data = response.json()
38
+
39
+ if "response" not in data:
40
+ raise ValueError(f"Unexpected API response format: {data}")
41
+
42
+ games = data["response"].get("games", [])
43
+ game_count = data["response"].get("game_count", 0)
44
+
45
+ # Convert playtime from minutes to hours for readability
46
+ for game in games:
47
+ if "playtime_forever" in game:
48
+ game["playtime_hours"] = round(game["playtime_forever"] / 60, 1)
49
+ if "playtime_2weeks" in game:
50
+ game["playtime_2weeks_hours"] = round(game["playtime_2weeks"] / 60, 1)
51
+
52
+ # Sort by playtime (most played first)
53
+ games_sorted = sorted(games, key=lambda g: g.get("playtime_forever", 0), reverse=True)
54
+
55
+ return {
56
+ "success": True,
57
+ "steam_id": steam_id,
58
+ "game_count": game_count,
59
+ "games": games_sorted
60
+ }
61
+
62
+ except httpx.HTTPStatusError as e:
63
+ raise ValueError(f"Steam API HTTP error: {e.response.status_code} - {e.response.text}")
64
+ except httpx.RequestError as e:
65
+ raise ValueError(f"Steam API request error: {str(e)}")
66
+ except Exception as e:
67
+ raise ValueError(f"Error fetching Steam library: {str(e)}")
68
+
69
+
70
+ def format_library_summary(library_data: Dict[str, Any], top_n: int = 20) -> str:
71
+ """
72
+ Format library data into a human-readable summary.
73
+
74
+ Args:
75
+ library_data: Output from get_steam_library()
76
+ top_n: Number of top games to include in summary
77
+
78
+ Returns:
79
+ Formatted string summary
80
+ """
81
+ if not library_data.get("success"):
82
+ return "Failed to fetch library data"
83
+
84
+ games = library_data.get("games", [])
85
+ game_count = library_data.get("game_count", 0)
86
+
87
+ if game_count == 0:
88
+ return "No games found in library (profile may be private)"
89
+
90
+ summary_lines = [
91
+ f"Steam Library Summary",
92
+ f"Total games: {game_count}",
93
+ f"\nTop {min(top_n, len(games))} most played games:",
94
+ "-" * 60
95
+ ]
96
+
97
+ for i, game in enumerate(games[:top_n], 1):
98
+ name = game.get("name", "Unknown")
99
+ hours = game.get("playtime_hours", 0)
100
+ appid = game.get("appid", "")
101
+
102
+ summary_lines.append(f"{i}. {name}")
103
+ summary_lines.append(f" Playtime: {hours} hours | AppID: {appid}")
104
+
105
+ # Add statistics
106
+ total_hours = sum(g.get("playtime_hours", 0) for g in games)
107
+ games_with_playtime = [g for g in games if g.get("playtime_forever", 0) > 0]
108
+ unplayed_games = game_count - len(games_with_playtime)
109
+
110
+ summary_lines.extend([
111
+ "",
112
+ "-" * 60,
113
+ f"Total playtime: {total_hours:,.1f} hours",
114
+ f"Games with playtime: {len(games_with_playtime)}",
115
+ f"Unplayed games: {unplayed_games}"
116
+ ])
117
+
118
+ return "\n".join(summary_lines)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ # Test the Steam API integration
123
+ import sys
124
+ from dotenv import load_dotenv
125
+
126
+ load_dotenv()
127
+
128
+ test_steam_id = "76561198162013924"
129
+ if len(sys.argv) > 1:
130
+ test_steam_id = sys.argv[1]
131
+
132
+ print(f"Fetching Steam library for ID: {test_steam_id}\n")
133
+
134
+ try:
135
+ library = get_steam_library(test_steam_id)
136
+ summary = format_library_summary(library)
137
+ print(summary)
138
+ except Exception as e:
139
+ print(f"Error: {e}")
140
+ sys.exit(1)