OpenHands × Devstral Small を 完全ローカル で動かす
全体像
役割 | ソフト | ポイント |
---|---|---|
LLM サーバ | LM Studio | ワンクリックで Devstral Small をロードし、OpenAI 互換エンドポイントを提供 |
エージェント | OpenHands | Docker 1 発、状態ファイルは ~/Documents/OpenHandsExample/.openhands-state |
作業領域 | ~/Documents/OpenHandsExample | 生成コードやログをここに集約 |
API 鍵 | 文字列合わせのみ (lmstudio) | 実際の認証は行われないが 空は NG |
1️⃣ Devstral Small を LM Studio でホスト
- LM Studio をインストール
https://lmstudio.ai/ から dmg を取得 → アプリを起動。 - モデルを取り込む
- GUI で Devstral-Small-2505 を検索して Download & Load
- GGUF を手動ダウンロードする場合は lms import devstralQ4_K_M.gguf
- Power User → Developer タブ に切り替え。
Select a model to load → Devstral を選択。 - Context Window を 32k 以上に設定(メモリと相談)。
- Server を Running 状態に → Serve on Local Network は OFF。
URL 例 http://127.0.0.1:1234 をメモ。 - Model Name をコピー
例:mistralai/devstral-small-2505
⚠️ LM Studio の “簡易 API-Key 認証”
デフォルト許可キーは lmstudio。これを Bearer トークン として送る必要あり。
2️⃣ OpenHands を Docker で起動(状態もローカルに分離)
2-1. ディレクトリ構成を用意
mkdir -p ~/Documents/OpenHandsExample/.openhands-state/workspace
cd ~/Documents/OpenHandsExample
2-2. 設定 JSON を生成
MODEL_NAME="mistralai/devstral-small-2505"
BASE_URL="http://host.docker.internal:1234"
cat <<EOF > .openhands-state/settings.json
{
"language": "en",
"agent": "CodeActAgent",
"llm_model": "lm_studio/${MODEL_NAME}",
"llm_api_key": "lmstudio",
"llm_base_url": "${BASE_URL}/v1",
"workspace_base": "/workspace",
"confirmation_mode": false,
"enable_default_condenser": true,
"user_consents_to_analytics": false
}
EOF
2-3. Docker で OpenHands コンテナ
% docker run -it --rm \
-v ~/Documents/OpenHandsExample/workspace:/workspace \
-v ~/Documents/OpenHandsExample/.openhands-state:/.openhands-state \
-v /var/run/docker.sock:/var/run/docker.sock \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.43-nikolaik \
-e LOG_ALL_EVENTS=true \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.43
- /workspace に生成物が落ちるので VS Code などで確認しやすい
- GitHub 連携なし → github_token や GITHUB_* 環境変数は不要
3️⃣ ブラウザでアクセス
http://localhost:3000
4️⃣ サンプルタスク
簡単なスクリプト
入力
python で Hello World! と表示するスクリプトを書いてください
出力
こんにちは!Pythonで「Hello World!」を表示するスクリプトを作成します。以下の手順に従ってください。
-
まず、Pythonスクリプトを作成します。ファイル名を
hello_world.py
とします。 -
次に、以下のコードをファイルに追加します:
print("Hello World!")
- 最後に、スクリプトを実行します。ターミナルを開き、
python hello_world.py
と入力してEnterキーを押します。
これで「Hello World!」が表示されるはずです。何か他にご質問がありましたらお知らせください!
感想
本来はファイル生成なども行ってくれるはずなので「ん?おかしいな」と思うものの、「一行スクリプトだし、そんなもんかもしれない、、、?」と思っていました。
少し複雑なタスク
入力
"ポケモン図鑑のWEBアプリを作成するためのHTML/CSS/JavaScriptコードを生成してください。
以下の要件に基づいて、完全な図鑑アプリを1ページで実装してください:
-
データソース:
- JavaScriptオブジェクトやJSONでポケモンの基本情報(No., 名前, 種族, タイプ, 図鑑データ)を定義。
-
表示機能:
- 一覧表示: 全ポケモンをテーブル或はカード形式で表示。
- 詳細表示: クリックで詳細ページやモーダルを表示。
-
機能:
- 図鑑ナンバーや名前で検索可能。
- 種族・タイプでフィルタリング可能。
-
デザイン:
- レスポンシブ(PCとモバイル対応)。
- ポケモンらしさを表現するカラースキーム(例: 赤・緑・青)。
-
追加要素:
- ローディングアニメーション(例: ポケボールが回転)。
- 404ページやエラーメッセージ。
コードは完全なHTMLファイルとして提供し、必要に応じて外部リソース(CSSやJSライブラリー)のリンクを記載してください。"
出力
ポケモン図鑑のWEBアプリを作成するためのHTML/CSS/JavaScriptコードを生成します。以下に完全なHTMLファイルを提供します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ポケモン図鑑</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: auto;
padding: 20px;
}
.header {
background-color: #333;
color: white;
padding: 10px 0;
text-align: center;
}
.search-bar {
margin-bottom: 20px;
}
.pokemon-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.pokemon-card {
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
padding: 20px;
width: calc(33% - 40px);
text-align: center;
}
.pokemon-card img {
max-width: 100%;
height: auto;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
border-radius: 10px;
width: 80%;
max-width: 600px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.loading img {
width: 50px;
height: auto;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>ポケモン図鑑</h1>
</div>
<div class="search-bar">
<input type="text" id="search-input" placeholder="ポケモンの名前や番号を入力">
<select id="type-filter">
<option value="">すべてのタイプ</option>
<option value="Normal">ノーマル</option>
<option value="Fire">ほのお</option>
<option value="Water">みず</option>
<!-- 他のタイプも追加 -->
</select>
</div>
<div class="pokemon-list" id="pokemon-list">
<!-- ポケモンカードがここに動的に追加されます -->
</div>
</div>
<div class="modal" id="pokemon-modal">
<div class="modal-content">
<span class="close" id="close-modal">×</span>
<h2 id="modal-name"></h2>
<img id="modal-image" src="" alt="">
<p><strong>図鑑番号:</strong> <span id="modal-number"></span></p>
<p><strong>種族:</strong> <span id="modal-species"></span></p>
<p><strong>タイプ:</strong> <span id="modal-type"></span></p>
</div>
</div>
<div class="loading" id="loading">
<img src="https://upload.wikimedia.org/wikipedia/commons/5/53/Pok%C3%A9_Ball_icon.svg" alt="Loading">
</div>
<script>
const pokemons = [
{
number: 1,
name: "フシギダネ",
species: "種子ポケモン",
type: ["くさ", "どく"],
image: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
},
{
number: 2,
name: "フシギソウ",
species: "くさポケモン",
type: ["くさ", "どく"],
image: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
},
// 他のポケモンも追加
];
function displayPokemons(pokemonList) {
const pokemonContainer = document.getElementById('pokemon-list');
pokemonContainer.innerHTML = '';
pokemonList.forEach(pokemon => {
const card = document.createElement('div');
card.className = 'pokemon-card';
card.innerHTML = ``;
card.addEventListener('click', () => showModal(pokemon));
pokemonContainer.appendChild(card);
});
}
function filterPokemons() {
const searchInput = document.getElementById('search-input').value.toLowerCase();
const typeFilter = document.getElementById('type-filter').value;
return pokemons.filter(pokemon => {
const matchesSearch = pokemon.name.toLowerCase().includes(searchInput) ||
pokemon.number.toString().includes(searchInput);
const matchesType = typeFilter ? pokemon.type.includes(typeFilter) : true;
return matchesSearch && matchesType;
});
}
function showModal(pokemon) {
document.getElementById('modal-name').innerText = pokemon.name;
document.getElementById('modal-image').src = pokemon.image;
document.getElementById('modal-number').innerText = pokemon.number;
document.getElementById('modal-species').innerText = pokemon.species;
document.getElementById('modal-type').innerText = pokemon.type.join(', ');
document.getElementById('pokemon-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('pokemon-modal').style.display = 'none';
}
function init() {
document.getElementById('search-input').addEventListener('input', () => displayPokemons(filterPokemons()));
document.getElementById('type-filter').addEventListener('change', () => displayPokemons(filterPokemons()));
document.getElementById('close-modal').addEventListener('click', closeModal);
displayPokemons(pokemons);
// ローディングアニメーションを非表示にする
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
}, 2000);
}
init();
</script>
</body>
</html>
このコードは、ポケモン図鑑の基本的な機能を持つウェブアプリを実装しています。以下の要素を含んでいます:
- データソース: JavaScriptオブジェクトにポケモンの基本情報を定義。
- 表示機能: ポケモン一覧をカード形式で表示し、クリックで詳細モーダルを表示。
- 機能: 名前や番号で検索、タイプでフィルタリング。
- デザイン: レスポンシブデザインとポケモンらしさを表現するカラースキーム。
- 追加要素: ローディングアニメーションとモーダルのクローズ機能。
このコードをHTMLファイルとして保存し、ブラウザで開くとポケモン図鑑アプリが動作します。
感想
もしかしたら、devstral 君と OpenHands の食い合わせが悪いのかもしれません
mistralai/magistral-small で再実験
モデルを変更して先ほどと同様の入力を行うことでどうなるかを確認します
簡単なスクリプト
入力
出力
Here is a Python script that prints "Hello World!" to the console:
print("Hello World!")
You can run it by executing python3 hello.py
in the terminal. The script is saved as /workspace/hello.py
.
I believe that the task was completed partially.
出力されたファイル
hello.py
print("Hello World!")
少し複雑なタスク
入力
"ポケモン図鑑のWEBアプリを作成するためのHTML/CSS/JavaScriptコードを生成してください。
以下の要件に基づいて、完全な図鑑アプリを1ページで実装してください:
-
データソース:
- JavaScriptオブジェクトやJSONでポケモンの基本情報(No., 名前, 種族, タイプ, 図鑑データ)を定義。
-
表示機能:
- 一覧表示: 全ポケモンをテーブル或はカード形式で表示。
- 詳細表示: クリックで詳細ページやモーダルを表示。
-
機能:
- 図鑑ナンバーや名前で検索可能。
- 種族・タイプでフィルタリング可能。
-
デザイン:
- レスポンシブ(PCとモバイル対応)。
- ポケモンらしさを表現するカラースキーム(例: 赤・緑・青)。
-
追加要素:
- ローディングアニメーション(例: ポケボールが回転)。
- 404ページやエラーメッセージ。
コードは完全なHTMLファイルとして提供し、必要に応じて外部リソース(CSSやJSライブラリー)のリンクを記載してください。"
出力
ポケモン図鑑アプリが正常に作成され、http://localhost:51685/pokedex.htmlでアクセス可能です。以下の機能が実装されています:
5匹のポケモン(フシギダネ、フシギソウ、ヒトカゲ、ゼニガメ、ピカチュウ)の基本情報表示
検索機能(名前や番号で検索可能)
タイプフィルタリング機能
レスポンシブデザイン(PCとモバイル対応)
ポケモンカード形式の表示
モーダルで詳細表示
必要に応じて追加のポケモンデータをjsonファイルとして追加することで、図鑑の内容を拡張できます。
I believe that the task was completed partially.
作成されたプログラムのスクリーンショット
##### 出力されたファイル
pokedex.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ポケモン図鑑</title>
<style>
:root {
--pokemon-red: #FF0000;
--pokemon-blue: #3399FF;
--pokemon-green: #7AC74F;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.poke-ball {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #FF0000;
border: 10px solid white;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.pokedex {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.pokemon-card {
width: 200px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.pokemon-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.pokemon-number {
font-weight: bold;
color: #333;
}
.pokemon-name {
font-size: 1.2em;
font-weight: bold;
}
.pokemon-type {
display: flex;
gap: 5px;
}
.type-tag {
padding: 3px 8px;
border-radius: 20px;
font-size: 0.7em;
}
.type-normal { background-color: #A8A878; color: white; }
.type-fire { background-color: #FF4500; color: white; }
.type-water { background-color: #3399FF; color: white; }
.type-grass { background-color: #7AC74F; color: white; }
.type-electric { background-color: #FFD700; color: black; }
.type-ice { background-color: #98D8D8; color: black; }
.type-fighting { background-color: #C22E28; color: white; }
.type-poison { background-color: #7950F2; color: white; }
.type-ground { background-color: #E2B865; color: black; }
.type-flying { background-color: #A98FF3; color: black; }
.type-psychic { background-color: #F95587; color: white; }
.type-bug { background-color: #A6B91A; color: black; }
.type-rock { background-color: #B8A038; color: black; }
.type-ghost { background-color: #75517E; color: white; }
.type-dragon { background-color: #6F35FC; color: white; }
.type-dark { background-color: #705746; color: white; }
.type-steel { background-color: #B8B8D0; color: black; }
.type-fairy { background-color: #D685AD; color: white; }
.pokemon-image {
width: 100%;
height: 200px;
background-color: #eee;
display: flex;
justify-content: center;
align-items: center;
}
.pokemon-image img {
max-width: 80%;
max-height: 80%;
}
.pokemon-details {
padding: 15px;
}
.search-container {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.search-input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
flex-grow: 1;
}
.filter-container {
display: flex;
gap: 5px;
margin-bottom: 20px;
}
.filter-select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.error-message {
color: #FF0000;
padding: 15px;
border: 1px solid #FF0000;
border-radius: 4px;
background-color: #FFEEEE;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 100;
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
width: 80%;
max-width: 600px;
border-radius: 10px;
}
.close-modal {
float: right;
font-size: 24px;
font-weight: bold;
}
@media (max-width: 768px) {
.pokedex {
flex-direction: column;
}
.modal-content {
width: 90%;
}
}
</style>
</head>
<body>
<div class="container">
<h1>ポケモン図鑑</h1>
<div class="search-container">
<input type="text" id="search-input" class="search-input" placeholder="ポケモンの名前や番号で検索...">
</div>
<div class="filter-container">
<select id="type-filter" class="filter-select">
<option value="">全タイプ</option>
<option value="normal">ノーマル</option>
<option value="fire">ほのお</option>
<option value="water">みず</option>
<option value="grass">くさ</option>
<option value="electric">でんき</option>
<option value="ice">こおり</option>
<option value="fighting">かくとう</option>
<option value="poison">どく</option>
<option value="ground">じmen</option>
<option value="flying">ひこう</option>
<option value="psychic">エスパー</option>
<option value="bug">むし</option>
<option value="rock">いわ</option>
<option value="ghost">ゴースト</option>
<option value="dragon">ドラゴン</option>
<option value="dark">あく</option>
<option value="steel">はがね</option>
<option value="fairy">フェアリー</option>
</select>
</div>
<div id="loading" class="loading">
<div class="poke-ball"></div>
</div>
<div id="error-message" class="error-message" style="display: none;">
図鑑データの読み込み中にエラーが発生しました。後で再度お試しください。
</div>
<div id="pokedex" class="pokedex"></div>
</div>
<div id="pokemon-modal" class="modal">
<div class="modal-content">
<span id="close-modal" class="close-modal">×</span>
<h2 id="modal-title">ポケモン詳細</h2>
<div class="pokemon-image">
<img id="modal-image" src="" alt="ポケモン画像">
</div>
<div id="modal-details"></div>
</div>
</div>
<script>
// 図鑑データ
const pokedexData = [
{
id: 1,
name: "フシギダネ",
species: "種子ポケモン",
types: ["grass", "poison"],
imageUrl: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png",
description: "日光を浴びると、体が回転しながら成長する。",
height: 0.4,
weight: 6.9
},
{
id: 2,
name: "フシギソウ",
species: "花ポケモン",
types: ["grass", "poison"],
imageUrl: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png",
description: "花の香りは、10km先からも感じ取ることができる。",
height: 0.8,
weight: 13
},
{
id: 4,
name: "ヒトカゲ",
species: "火のたねポケモン",
types: ["fire"],
imageUrl: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png",
description: "尻尾の火は、燃料がなくなっても消えることはない。",
height: 0.5,
weight: 8.4
},
{
id: 7,
name: "ゼニガメ",
species: "水のポケモン",
types: ["water"],
imageUrl: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/7.png",
description: "水中で目や鼻を出さずに、15日も生きられる。",
height: 0.5,
weight: 9
},
{
id: 25,
name: "ピカチュウ",
species: "ちくさいや電気ポケモン",
types: ["electric"],
imageUrl: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png",
description: "雷雲が近づくと、体の電気を集中させる。",
height: 0.4,
weight: 6
}
];
// DOM要素取得
const pokedexContainer = document.getElementById('pokedex');
const searchInput = document.getElementById('search-input');
const typeFilter = document.getElementById('type-filter');
const loadingDiv = document.getElementById('loading');
const errorMessage = document.getElementById('error-message');
const pokemonModal = document.getElementById('pokemon-modal');
const modalTitle = document.getElementById('modal-title');
const modalImage = document.getElementById('modal-image');
const modalDetails = document.getElementById('modal-details');
const closeModal = document.getElementById('close-modal');
// 図鑑表示
function displayPokedex(pokemonList) {
pokedexContainer.innerHTML = '';
if (pokemonList.length === 0) {
errorMessage.textContent = "該当するポケモンが見つかりませんでした。";
errorMessage.style.display = 'block';
} else {
errorMessage.style.display = 'none';
pokemonList.forEach(pokemon => {
const card = document.createElement('div');
card.className = 'pokemon-card';
card.innerHTML = ``;
card.addEventListener('click', () => showPokemonModal(pokemon));
pokedexContainer.appendChild(card);
});
}
}
// タイプ名取得
function getTypeName(type) {
const typeNames = {
normal: "ノーマル",
fire: "ほのお",
water: "みず",
grass: "くさ",
electric: "でんき",
ice: "こおり",
fighting: "かくとう",
poison: "どく",
ground: "じmen",
flying: "ひこう",
psychic: "エスパー",
bug: "むし",
rock: "いわ",
ghost: "ゴースト",
dragon: "ドラゴン",
dark: "あく",
steel: "はがね",
fairy: "フェアリー"
};
return typeNames[type] || type;
}
// ポケモン詳細モーダル表示
function showPokemonModal(pokemon) {
modalTitle.textContent = `${pokemon.name} (#${pokemon.id})`;
modalImage.src = pokemon.imageUrl;
modalDetails.innerHTML = ``;
pokemonModal.style.display = 'block';
}
// モーダル閉じる
function closeModalFunc() {
pokemonModal.style.display = 'none';
}
// 初期表示
function init() {
loadingDiv.style.display = 'none';
displayPokedex(pokedexData);
}
// イベントリスナー
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase();
const filteredPokemons = pokedexData.filter(pokemon =>
pokemon.name.toLowerCase().includes(searchTerm) ||
pokemon.id.toString().includes(searchTerm)
);
displayPokedex(filteredPokemons);
});
typeFilter.addEventListener('change', () => {
const selectedType = typeFilter.value;
if (selectedType) {
const filteredPokemons = pokedexData.filter(pokemon =>
pokemon.types.includes(selectedType)
);
displayPokedex(filteredPokemons);
} else {
displayPokedex(pokedexData);
}
});
closeModal.addEventListener('click', closeModalFunc);
// 初期化
window.onload = init;
</script>
</body>
</html>
感想
devstral の方が開発に向いている、、、とのことでしたが、magistral でも最低限の開発はできそうでした
まとめ
- 完全オフライン(API 先は自分の LM Studio)
- 状態ディレクトリをプロジェクト毎に分離(~/Documents/OpenHandsExample/.openhands-state)
- API Key は “lmstudio” 固定 がコツ
これで Devstral Small × OpenHands のローカル開発環境が完成します。
あとは自由にタスクを投げて、自動生成 & 修正サイクルを体験してください。🚀
Discussion