Spaces:
Running
Running
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>
- .gitignore +50 -0
- CLAUDE.md +158 -0
- app.py +106 -12
- 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 |
-
"""
|
| 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 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|