-
-
Save laiso/c5e1bb2f8a230d218b9e75e08b97a3aa to your computer and use it in GitHub Desktop.
PTC Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Tools package for PTC token comparison demo | |
| """ | |
| from .count_words import count_words | |
| from .filter_results import filter_results | |
| from .save_summary import save_summary | |
| __all__ = ['count_words', 'filter_results', 'save_summary'] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| count_wordsツール: テキストファイルから単語頻度を分析する | |
| 要件 3.1, 3.2, 3.3 を実装 | |
| """ | |
| import os | |
| import MeCab | |
| from collections import Counter | |
| from typing import List, Dict | |
| # ストップワード(助詞、助動詞、一般的な動詞など) | |
| STOP_WORDS = { | |
| # 助詞・助動詞 | |
| 'の', 'に', 'は', 'を', 'た', 'が', 'で', 'て', 'と', 'し', 'れ', 'さ', | |
| 'ある', 'いる', 'も', 'する', 'から', 'な', 'こと', 'として', 'い', | |
| 'や', 'れる', 'など', 'なっ', 'ない', 'この', 'ため', 'その', 'あっ', | |
| 'よう', 'また', 'もの', 'という', 'あり', 'まで', 'られ', 'なる', | |
| 'へ', 'か', 'だ', 'これ', 'によって', 'により', 'おり', 'より', 'による', | |
| 'ず', 'なり', 'られる', 'において', 'ば', 'なかっ', 'なく', 'しかし', | |
| 'について', 'せ', 'だっ', 'その後', 'できる', 'それ', 'う', 'ので', | |
| 'なお', 'のみ', 'でき', 'き', 'つ', 'における', 'および', 'いう', | |
| 'さらに', 'でも', 'ら', 'たり', 'その他', 'に関する', 'たち', 'ます', | |
| 'ん', 'なら', 'に対して', '及び', 'これら', 'とき', 'では', 'にて', | |
| # カタカナ表記の一般動詞(MeCabの基本形出力) | |
| 'スル', 'イル', 'アル', 'ナル', 'デキル', 'イウ', 'コト', 'モノ', | |
| 'ヨウ', 'トキ', 'トコロ', 'ワケ', 'タメ', 'ホウ', 'ツモリ', 'ハズ', | |
| 'ミル', 'オモウ', 'カンガエル', 'シマウ', 'オク', 'クル', 'イク', | |
| 'クレル', 'モラウ', 'アゲル', 'ヤル', 'サセル', 'レル', 'ラレル' | |
| } | |
| def count_words(filename: str) -> List[Dict[str, any]]: | |
| """ | |
| テキストファイルから単語頻度を分析する | |
| Args: | |
| filename: 分析対象のテキストファイル名(パスはツール側で解決) | |
| Returns: | |
| [ | |
| {"word": "単語", "count": 123}, | |
| ... | |
| ] | |
| 頻度順にソートされた全単語リスト | |
| Raises: | |
| FileNotFoundError: ファイルが見つからない場合 | |
| Exception: MeCabの初期化や解析に失敗した場合 | |
| """ | |
| try: | |
| # パスを解決:カレントディレクトリまたは /workspace/ を試す | |
| possible_paths = [ | |
| filename, # カレントディレクトリ | |
| os.path.join(os.getcwd(), filename), # 明示的なカレントディレクトリ | |
| os.path.join('/workspace', filename), # サンドボックス内のワークスペース | |
| os.path.join('/tmp', filename), # サンドボックス内のtmp | |
| ] | |
| path = None | |
| for p in possible_paths: | |
| if os.path.exists(p): | |
| path = p | |
| break | |
| if path is None: | |
| raise FileNotFoundError(f"ファイルが見つかりません: {filename}") | |
| # ファイルを読み込む | |
| with open(path, 'r', encoding='utf-8') as f: | |
| text = f.read() | |
| if not text.strip(): | |
| return [] | |
| # MeCabで形態素解析 | |
| tagger = MeCab.Tagger() | |
| tagger.parse('') # 初期化のためのダミー呼び出し | |
| node = tagger.parseToNode(text) | |
| words = [] | |
| while node: | |
| # 品詞情報を取得 | |
| features = node.feature.split(',') | |
| pos = features[0] # 品詞 | |
| # 名詞、動詞、形容詞のみを抽出 | |
| if pos in ['名詞', '動詞', '形容詞']: | |
| # 基本形を取得(利用可能な場合) | |
| if len(features) >= 7 and features[6] != '*': | |
| word = features[6] | |
| else: | |
| word = node.surface | |
| # ストップワードと1文字の単語を除外 | |
| if word and len(word) > 1 and word not in STOP_WORDS: | |
| words.append(word) | |
| node = node.next | |
| # 単語頻度をカウント | |
| word_counts = Counter(words) | |
| # 頻度順にソート(降順) | |
| sorted_words = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) | |
| # JSON配列形式に変換 | |
| result = [ | |
| {"word": word, "count": count} | |
| for word, count in sorted_words | |
| ] | |
| return result | |
| except FileNotFoundError as e: | |
| raise e | |
| except Exception as e: | |
| raise Exception(f"単語頻度分析中にエラーが発生しました: {str(e)}") | |
| if __name__ == "__main__": | |
| # テスト用 | |
| import sys | |
| if len(sys.argv) > 1: | |
| result = count_words(sys.argv[1]) | |
| print(f"Total words: {len(result)}") | |
| print("Top 10 words:") | |
| for item in result[:10]: | |
| print(f" {item['word']}: {item['count']}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| filter_resultsツール: 単語頻度データをフィルタリングする | |
| 要件 4.1, 4.2, 4.3 を実装 | |
| """ | |
| from typing import List, Dict | |
| def filter_results(words: List[Dict[str, any]], min_freq: int) -> List[Dict[str, any]]: | |
| """ | |
| 単語頻度データをフィルタリングする | |
| Args: | |
| words: 単語頻度データの配列 | |
| [{"word": str, "count": int}, ...] | |
| min_freq: 最小頻度(この値以上の単語のみを返す) | |
| Returns: | |
| フィルタリングされた単語頻度データの配列 | |
| [{"word": str, "count": int}, ...] | |
| Raises: | |
| ValueError: 入力データの形式が不正な場合 | |
| """ | |
| try: | |
| # 入力検証 | |
| if not isinstance(words, list): | |
| raise ValueError("words must be a list") | |
| if not isinstance(min_freq, int): | |
| raise ValueError("min_freq must be an integer") | |
| # 各要素の形式を検証 | |
| for item in words: | |
| if not isinstance(item, dict): | |
| raise ValueError("Each item in words must be a dictionary") | |
| if 'word' not in item or 'count' not in item: | |
| raise ValueError("Each item must have 'word' and 'count' fields") | |
| if not isinstance(item['count'], int): | |
| raise ValueError("'count' field must be an integer") | |
| # min_freq以上の単語のみをフィルタリング | |
| filtered = [ | |
| item for item in words | |
| if item['count'] >= min_freq | |
| ] | |
| return filtered | |
| except ValueError as e: | |
| raise ValueError(f"入力データの検証エラー: {str(e)}") | |
| except Exception as e: | |
| raise Exception(f"フィルタリング中にエラーが発生しました: {str(e)}") | |
| if __name__ == "__main__": | |
| # テスト用 | |
| test_data = [ | |
| {"word": "テスト", "count": 50}, | |
| {"word": "データ", "count": 30}, | |
| {"word": "単語", "count": 15}, | |
| {"word": "頻度", "count": 8}, | |
| {"word": "分析", "count": 5}, | |
| ] | |
| print("Original data:") | |
| for item in test_data: | |
| print(f" {item['word']}: {item['count']}") | |
| print("\nFiltered (min_freq=10):") | |
| filtered = filter_results(test_data, 10) | |
| for item in filtered: | |
| print(f" {item['word']}: {item['count']}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| """ | |
| run_non_ptc.py | |
| Non-PTC(従来のTool Use)実装 | |
| Claude APIの従来のツール呼び出し方式を使用して3つのツールを連鎖的に呼び出し、 | |
| トークン使用量を測定する。 | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| from anthropic import Anthropic | |
| from dotenv import load_dotenv | |
| # 3つのツールをインポート | |
| from tools.count_words import count_words | |
| from tools.filter_results import filter_results | |
| from tools.save_summary import save_summary | |
| # 共通出力フォーマット関数をインポート | |
| from output_formatter import format_token_usage, print_mode_header | |
| def main(): | |
| """Non-PTCスクリプトのメイン処理(3ツール連鎖)""" | |
| # .envファイルから環境変数を読み込む | |
| load_dotenv() | |
| # 環境変数からAPI キーを読み込む | |
| api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| if not api_key: | |
| print("Error: ANTHROPIC_API_KEY環境変数が設定されていません") | |
| print("以下のコマンドで設定してください:") | |
| print("export ANTHROPIC_API_KEY='your-api-key'") | |
| sys.exit(1) | |
| # Anthropic クライアントを初期化 | |
| client = Anthropic(api_key=api_key) | |
| # パラメータ設定 | |
| target_file = "sample_text.txt" | |
| min_freq = 10 # フィルタリングの最小頻度 | |
| output_file = "result-non-ptc.txt" # 保存先ファイル | |
| # ファイルの存在確認 | |
| if not os.path.exists(target_file): | |
| print(f"Error: {target_file}が見つかりません") | |
| sys.exit(1) | |
| # 3つのツール定義(Non-PTC: allowed_callers=["direct"]) | |
| tools = [ | |
| { | |
| "name": "count_words", | |
| "description": "テキストファイルから単語頻度を分析し、JSON配列として返す。ファイル名のみを指定してください(パスはツール側で自動解決されます)。", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "filename": { | |
| "type": "string", | |
| "description": "分析対象のテキストファイル名(例: 'sample_text.txt')" | |
| } | |
| }, | |
| "required": ["filename"] | |
| }, | |
| "allowed_callers": ["direct"] | |
| }, | |
| { | |
| "name": "filter_results", | |
| "description": "単語頻度データをフィルタリングし、指定された最小頻度以上の単語のみを返す", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "words": { | |
| "type": "array", | |
| "description": "単語頻度データの配列", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "word": {"type": "string"}, | |
| "count": {"type": "integer"} | |
| } | |
| } | |
| }, | |
| "min_freq": { | |
| "type": "integer", | |
| "description": "最小頻度(この値以上の単語のみを返す)" | |
| } | |
| }, | |
| "required": ["words", "min_freq"] | |
| }, | |
| "allowed_callers": ["direct"] | |
| }, | |
| { | |
| "name": "save_summary", | |
| "description": "フィルタリングされた単語頻度データをファイルに保存する。ファイル名のみを指定してください(パスはツール側で自動解決されます)。", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "result": { | |
| "type": "array", | |
| "description": "保存する単語頻度データの配列", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "word": {"type": "string"}, | |
| "count": {"type": "integer"} | |
| } | |
| } | |
| }, | |
| "output_filename": { | |
| "type": "string", | |
| "description": "保存先ファイル名(例: 'result-non-ptc.txt')" | |
| } | |
| }, | |
| "required": ["result", "output_filename"] | |
| }, | |
| "allowed_callers": ["direct"] | |
| } | |
| ] | |
| # モード表示 | |
| print_mode_header("Non-PTC") | |
| # 実行時間の計測開始 | |
| start_time = time.time() | |
| # Claude API呼び出し(model: claude-sonnet-4-5) | |
| print("Claude APIを呼び出しています...") | |
| print(f"対象ファイル: {target_file}") | |
| print(f"最小頻度: {min_freq}") | |
| print(f"出力ファイル: {output_file}") | |
| print() | |
| # 会話履歴を保持 | |
| messages = [ | |
| { | |
| "role": "user", | |
| "content": f"""以下の3つのツールを順次呼び出して、単語頻度分析を実行してください: | |
| 1. count_words: ファイル名 "{target_file}" から単語頻度を分析(ファイル名のみ指定、パスは不要) | |
| 2. filter_results: 結果をフィルタリング(min_freq={min_freq}) | |
| 3. save_summary: フィルタリング結果をファイル名 "{output_file}" に保存(ファイル名のみ指定、パスは不要) | |
| 注意: | |
| - count_wordsとsave_summaryツールはファイル名のみを受け取ります(パスはツール側で自動解決されます) | |
| - count_wordsツールはMeCabによる形態素解析を使用しているため、結果はカタカナ表記の基本形(例:「カイハツ」→「開発」、「ジッコウ」→「実行」)で返されます | |
| 最後に、以下の情報を報告してください: | |
| - 保存が完了したこと | |
| - 頻出単語トップ5とその出現回数(カタカナ表記と推測される漢字表記の両方を表示) | |
| - 各頻出単語について、文章の多様性を高めるための類義語や置き換え表現の提案(日本語で3つずつ) | |
| 例:「カイハツ(開発)」→ 「構築」「実装」「作成」""" | |
| } | |
| ] | |
| # 初回メッセージ送信(betaヘッダーが必要) | |
| message = client.beta.messages.create( | |
| model="claude-sonnet-4-5", | |
| betas=["advanced-tool-use-2025-11-20"], | |
| max_tokens=8192, | |
| tools=tools, | |
| messages=messages | |
| ) | |
| # ツール実行ループ(3ツール順次呼び出し) | |
| while message.stop_reason == "tool_use": | |
| # tool_useブロックを処理 | |
| tool_results = [] | |
| for content_block in message.content: | |
| if content_block.type == "tool_use": | |
| tool_name = content_block.name | |
| tool_input = content_block.input | |
| tool_use_id = content_block.id | |
| print(f"ツール呼び出し: {tool_name}") | |
| # パラメータを短縮表示 | |
| compact_input = {} | |
| for key, value in tool_input.items(): | |
| if isinstance(value, list) and len(value) > 3: | |
| compact_input[key] = f"[{len(value)} items]" | |
| elif isinstance(value, str) and len(value) > 50: | |
| compact_input[key] = value[:50] + "..." | |
| else: | |
| compact_input[key] = value | |
| print(f" パラメータ: {json.dumps(compact_input, ensure_ascii=False)}") | |
| try: | |
| if tool_name == "count_words": | |
| # count_wordsツールを実行 | |
| result = count_words(filename=tool_input["filename"]) | |
| # 完全なJSON結果をtool_resultとして返送 | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps(result, ensure_ascii=False) | |
| }) | |
| print(f" → {len(result)}件の単語を返しました") | |
| elif tool_name == "filter_results": | |
| # filter_resultsツールを実行 | |
| result = filter_results( | |
| words=tool_input["words"], | |
| min_freq=tool_input["min_freq"] | |
| ) | |
| # 完全なJSON結果をtool_resultとして返送 | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps(result, ensure_ascii=False) | |
| }) | |
| print(f" → {len(result)}件の単語にフィルタリングしました") | |
| elif tool_name == "save_summary": | |
| # save_summaryツールを実行 | |
| result = save_summary( | |
| result=tool_input["result"], | |
| output_filename=tool_input["output_filename"] | |
| ) | |
| # ステータスをtool_resultとして返送 | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps(result, ensure_ascii=False) | |
| }) | |
| print(f" → ファイルに保存しました: {result['path']}") | |
| else: | |
| # 未知のツール | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps({"error": f"Unknown tool: {tool_name}"}, ensure_ascii=False), | |
| "is_error": True | |
| }) | |
| print(f" → エラー: 未知のツール") | |
| except Exception as e: | |
| # エラーが発生した場合 | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps({"error": str(e)}, ensure_ascii=False), | |
| "is_error": True | |
| }) | |
| print(f" → エラー: {e}") | |
| # 会話履歴にassistantの応答を追加 | |
| messages.append({ | |
| "role": "assistant", | |
| "content": message.content | |
| }) | |
| # tool_resultを追加 | |
| messages.append({ | |
| "role": "user", | |
| "content": tool_results | |
| }) | |
| # tool_resultを返送(会話履歴を引き継ぐ) | |
| message = client.beta.messages.create( | |
| model="claude-sonnet-4-5", | |
| betas=["advanced-tool-use-2025-11-20"], | |
| max_tokens=8192, | |
| tools=tools, | |
| messages=messages | |
| ) | |
| # 実行時間の計測終了 | |
| end_time = time.time() | |
| execution_time = end_time - start_time | |
| # 最終レスポンスを表示 | |
| print() | |
| print("=" * 60) | |
| print("Claude の回答:") | |
| print("=" * 60) | |
| for content_block in message.content: | |
| if hasattr(content_block, "text"): | |
| print(content_block.text) | |
| print() | |
| # トークン使用量の出力(input_tokens, output_tokens) | |
| print(format_token_usage( | |
| mode="Non-PTC", | |
| min_freq=min_freq, | |
| input_tokens=message.usage.input_tokens, | |
| output_tokens=message.usage.output_tokens, | |
| execution_time=execution_time | |
| )) | |
| if __name__ == "__main__": | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| """ | |
| run_ptc.py | |
| PTC(Programmatic Tool Calling)実装 | |
| Claude APIのPTC機能を使用して3つのツールを連鎖的に呼び出し、 | |
| トークン使用量を測定する。 | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| from anthropic import Anthropic | |
| from dotenv import load_dotenv | |
| # 3つのツールをインポート | |
| from tools.count_words import count_words | |
| from tools.filter_results import filter_results | |
| from tools.save_summary import save_summary | |
| # 共通出力フォーマット関数をインポート | |
| from output_formatter import format_token_usage, print_mode_header | |
| def main(): | |
| """PTCスクリプトのメイン処理""" | |
| # .envファイルから環境変数を読み込む | |
| load_dotenv() | |
| # 環境変数からAPI キーを読み込む | |
| api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| if not api_key: | |
| print("Error: ANTHROPIC_API_KEY環境変数が設定されていません") | |
| print("以下のコマンドで設定してください:") | |
| print("export ANTHROPIC_API_KEY='your-api-key'") | |
| sys.exit(1) | |
| # Anthropic クライアントを初期化 | |
| client = Anthropic(api_key=api_key) | |
| # 入力ファイルとパラメータ | |
| target_file = "sample_text.txt" | |
| if not os.path.exists(target_file): | |
| print(f"Error: {target_file}が見つかりません") | |
| sys.exit(1) | |
| min_freq = 10 # フィルタリングの最小頻度 | |
| output_file = "result-ptc.txt" # 保存先ファイル | |
| # 3つのツール定義を実装(各allowed_callers=["code_execution_20250825"]) | |
| tools = [ | |
| { | |
| "type": "code_execution_20250825", | |
| "name": "code_execution" | |
| }, | |
| { | |
| "name": "count_words", | |
| "description": "テキストファイルから単語頻度を分析し、頻度順にソートされた結果を返します。ファイル名のみを指定してください(パスはツール側で自動解決されます)。", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "filename": { | |
| "type": "string", | |
| "description": "分析対象のテキストファイル名(例: 'sample_text.txt')" | |
| } | |
| }, | |
| "required": ["filename"] | |
| }, | |
| "allowed_callers": ["code_execution_20250825"] | |
| }, | |
| { | |
| "name": "filter_results", | |
| "description": "単語頻度データをフィルタリングし、指定された最小頻度以上の単語のみを返します。", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "words": { | |
| "type": "array", | |
| "description": "単語頻度データの配列", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "word": {"type": "string"}, | |
| "count": {"type": "integer"} | |
| } | |
| } | |
| }, | |
| "min_freq": { | |
| "type": "integer", | |
| "description": "最小頻度(この値以上の単語のみを返す)" | |
| } | |
| }, | |
| "required": ["words", "min_freq"] | |
| }, | |
| "allowed_callers": ["code_execution_20250825"] | |
| }, | |
| { | |
| "name": "save_summary", | |
| "description": "フィルタリングされた単語頻度データをファイルに保存します。ファイル名のみを指定してください(パスはツール側で自動解決されます)。", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "result": { | |
| "type": "array", | |
| "description": "保存する単語頻度データの配列", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "word": {"type": "string"}, | |
| "count": {"type": "integer"} | |
| } | |
| } | |
| }, | |
| "output_filename": { | |
| "type": "string", | |
| "description": "保存先ファイル名(例: 'result-ptc.txt')" | |
| } | |
| }, | |
| "required": ["result", "output_filename"] | |
| }, | |
| "allowed_callers": ["code_execution_20250825"] | |
| } | |
| ] | |
| # モード表示("PTC") | |
| print_mode_header("PTC") | |
| # 実行時間の計測開始 | |
| start_time = time.time() | |
| # Claude API呼び出しの設定(model: claude-sonnet-4-5, beta: advanced-tool-use-2025-11-20) | |
| print("Claude APIを呼び出しています...") | |
| # プロンプト:3つのツールを連鎖的に呼び出し、最後に類義語を提案 | |
| messages = [ | |
| { | |
| "role": "user", | |
| "content": f"""以下の3つのツールを順次呼び出して、単語頻度分析を実行してください: | |
| 1. count_words: ファイル名 "{target_file}" から単語頻度を分析(ファイル名のみ指定、パスは不要) | |
| 2. filter_results: 結果をフィルタリング(min_freq={min_freq}) | |
| 3. save_summary: フィルタリング結果をファイル名 "{output_file}" に保存(ファイル名のみ指定、パスは不要) | |
| 注意: | |
| - count_wordsとsave_summaryツールはファイル名のみを受け取ります(パスはツール側で自動解決されます) | |
| - count_wordsツールはMeCabによる形態素解析を使用しているため、結果はカタカナ表記の基本形(例:「カイハツ」→「開発」、「ジッコウ」→「実行」)で返されます | |
| 最後に、以下の情報を報告してください: | |
| - 保存が完了したこと | |
| - 頻出単語トップ5とその出現回数(カタカナ表記と推測される漢字表記の両方を表示) | |
| - 各頻出単語について、文章の多様性を高めるための類義語や置き換え表現の提案(日本語で3つずつ) | |
| 例:「カイハツ(開発)」→ 「構築」「実装」「作成」""" | |
| } | |
| ] | |
| # 初回メッセージ送信 | |
| message = client.beta.messages.create( | |
| model="claude-sonnet-4-5", | |
| betas=["advanced-tool-use-2025-11-20"], | |
| max_tokens=8192, | |
| tools=tools, | |
| messages=messages | |
| ) | |
| # container IDを保持(PTCのサンドボックス維持に必須) | |
| container_id = None | |
| if hasattr(message, 'container') and message.container: | |
| container_id = message.container.id | |
| # サーバーサイドツール実行の処理を実装 | |
| # stop_reasonが"tool_use"の間ループ(PTCの正しいライフサイクル) | |
| iteration = 0 | |
| while message.stop_reason == "tool_use": | |
| iteration += 1 | |
| print(f"\n--- イテレーション {iteration} ---") | |
| # server_tool_useとtool_useを分離して処理 | |
| server_tool_use_blocks = [] | |
| tool_use_blocks = [] | |
| for content_block in message.content: | |
| if content_block.type == "server_tool_use": | |
| server_tool_use_blocks.append(content_block) | |
| # Claudeが生成したPythonコードをログ出力(デバッグ用) | |
| print(f"\n[server_tool_use] id={content_block.id}, name={content_block.name}") | |
| if hasattr(content_block, 'input') and 'code' in content_block.input: | |
| print("生成されたPythonコード:") | |
| print("-" * 40) | |
| print(content_block.input['code']) | |
| print("-" * 40) | |
| elif content_block.type == "server_tool_result": | |
| # サンドボックスからの実行結果をログ出力 | |
| print(f"\n[server_tool_result] id={content_block.id}") | |
| if hasattr(content_block, 'output'): | |
| print("サンドボックス実行結果:") | |
| print("-" * 40) | |
| print(content_block.output) | |
| print("-" * 40) | |
| elif content_block.type == "tool_use": | |
| tool_use_blocks.append(content_block) | |
| # サーバーサイドツール実行ループを実装(3ツール連鎖) | |
| tool_results = [] | |
| for tool_use_block in tool_use_blocks: | |
| tool_name = tool_use_block.name | |
| tool_input = tool_use_block.input | |
| tool_use_id = tool_use_block.id | |
| print(f"\n[tool_use] id={tool_use_id}, name={tool_name}") | |
| # パラメータを短縮表示 | |
| compact_input = {} | |
| for key, value in tool_input.items(): | |
| if isinstance(value, list) and len(value) > 3: | |
| compact_input[key] = f"[{len(value)} items]" | |
| elif isinstance(value, str) and len(value) > 50: | |
| compact_input[key] = value[:50] + "..." | |
| else: | |
| compact_input[key] = value | |
| print(f"パラメータ: {json.dumps(compact_input, ensure_ascii=False)}") | |
| try: | |
| if tool_name == "count_words": | |
| # count_words関数を実行 | |
| result = count_words(filename=tool_input["filename"]) | |
| # 結果をtool_resultとして準備(サンドボックスへ) | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps(result, ensure_ascii=False) | |
| }) | |
| print(f"結果: {len(result)}件の単語を返しました") | |
| elif tool_name == "filter_results": | |
| # filter_results関数を実行 | |
| result = filter_results( | |
| words=tool_input["words"], | |
| min_freq=tool_input["min_freq"] | |
| ) | |
| # 結果をtool_resultとして準備(サンドボックスへ) | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps(result, ensure_ascii=False) | |
| }) | |
| print(f"結果: {len(result)}件の単語にフィルタリングしました") | |
| elif tool_name == "save_summary": | |
| # save_summary関数を実行 | |
| result = save_summary( | |
| result=tool_input["result"], | |
| output_filename=tool_input["output_filename"] | |
| ) | |
| # 簡潔なステータスのみをtool_resultとして準備(サンドボックスへ) | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps(result, ensure_ascii=False) | |
| }) | |
| print(f"結果: ファイル保存完了 - {result['path']}") | |
| except Exception as e: | |
| # エラーが発生した場合 | |
| tool_results.append({ | |
| "type": "tool_result", | |
| "tool_use_id": tool_use_id, | |
| "content": json.dumps({"error": str(e)}, ensure_ascii=False), | |
| "is_error": True | |
| }) | |
| print(f"エラー: {e}") | |
| # 会話履歴にassistantの応答を追加 | |
| messages.append({ | |
| "role": "assistant", | |
| "content": message.content | |
| }) | |
| # tool_resultを追加 | |
| messages.append({ | |
| "role": "user", | |
| "content": tool_results | |
| }) | |
| # 次のAPI呼び出し(会話履歴とcontainerを引き継ぐ) | |
| request_params = { | |
| "model": "claude-sonnet-4-5", | |
| "betas": ["advanced-tool-use-2025-11-20"], | |
| "max_tokens": 8192, | |
| "tools": tools, | |
| "messages": messages | |
| } | |
| # container IDがあれば引き継ぐ(サンドボックス維持に必須) | |
| if container_id: | |
| request_params["container"] = container_id | |
| message = client.beta.messages.create(**request_params) | |
| # container IDを更新 | |
| if hasattr(message, 'container') and message.container: | |
| container_id = message.container.id | |
| # 実行時間の計測終了 | |
| end_time = time.time() | |
| execution_time = end_time - start_time | |
| # 最終レスポンスを表示(サンドボックスからのprint出力のみ) | |
| print() | |
| print("=" * 60) | |
| print("Claude の回答:") | |
| print("=" * 60) | |
| for content_block in message.content: | |
| if hasattr(content_block, "text"): | |
| print(content_block.text) | |
| print() | |
| # トークン使用量の出力処理を実装(input_tokens, output_tokens, total_tokens) | |
| print(format_token_usage( | |
| mode="PTC", | |
| min_freq=min_freq, | |
| input_tokens=message.usage.input_tokens, | |
| output_tokens=message.usage.output_tokens, | |
| execution_time=execution_time | |
| )) | |
| if __name__ == "__main__": | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| save_summaryツール: フィルタリングされた結果をファイルに保存する | |
| 要件 5.1, 5.2, 5.3, 5.4 を実装 | |
| """ | |
| import os | |
| import json | |
| from typing import List, Dict | |
| def save_summary(result: List[Dict[str, any]], output_filename: str) -> Dict[str, str]: | |
| """ | |
| フィルタリングされた結果をファイルに保存する | |
| Args: | |
| result: 保存する単語頻度データの配列 | |
| [{"word": str, "count": int}, ...] | |
| output_filename: 保存先ファイル名(パスはツール側で解決) | |
| Returns: | |
| {"status": "success", "path": 実際の保存先パス} | |
| Raises: | |
| ValueError: 入力データの形式が不正な場合 | |
| IOError: ファイル保存に失敗した場合 | |
| """ | |
| try: | |
| # 入力検証 | |
| if not isinstance(result, list): | |
| raise ValueError("result must be a list") | |
| if not isinstance(output_filename, str) or not output_filename: | |
| raise ValueError("output_filename must be a non-empty string") | |
| # 各要素の形式を検証 | |
| for item in result: | |
| if not isinstance(item, dict): | |
| raise ValueError("Each item in result must be a dictionary") | |
| if 'word' not in item or 'count' not in item: | |
| raise ValueError("Each item must have 'word' and 'count' fields") | |
| # パスを解決:カレントディレクトリまたは /workspace/ を試す | |
| possible_dirs = [ | |
| os.getcwd(), # カレントディレクトリ | |
| '/workspace', # サンドボックス内のワークスペース | |
| '/tmp', # サンドボックス内のtmp | |
| ] | |
| output_path = None | |
| for directory in possible_dirs: | |
| try: | |
| test_path = os.path.join(directory, output_filename) | |
| # ディレクトリが書き込み可能かテスト | |
| if os.path.isdir(directory) and os.access(directory, os.W_OK): | |
| output_path = test_path | |
| break | |
| except: | |
| continue | |
| if output_path is None: | |
| # フォールバック:カレントディレクトリに保存 | |
| output_path = output_filename | |
| # 人間が読みやすい形式でファイルに保存 | |
| with open(output_path, 'w', encoding='utf-8') as f: | |
| f.write("単語頻度分析結果\n") | |
| f.write("=" * 50 + "\n\n") | |
| f.write(f"総単語数: {len(result)}\n\n") | |
| f.write("-" * 50 + "\n") | |
| f.write(f"{'単語':<20} {'頻度':>10}\n") | |
| f.write("-" * 50 + "\n") | |
| for item in result: | |
| word = item['word'] | |
| count = item['count'] | |
| f.write(f"{word:<20} {count:>10}\n") | |
| f.write("-" * 50 + "\n") | |
| return { | |
| "status": "success", | |
| "path": output_path | |
| } | |
| except ValueError as e: | |
| raise ValueError(f"入力データの検証エラー: {str(e)}") | |
| except IOError as e: | |
| raise IOError(f"ファイル保存エラー: {str(e)}") | |
| except Exception as e: | |
| raise Exception(f"保存処理中にエラーが発生しました: {str(e)}") | |
| if __name__ == "__main__": | |
| # テスト用 | |
| test_data = [ | |
| {"word": "テスト", "count": 50}, | |
| {"word": "データ", "count": 30}, | |
| {"word": "単語", "count": 15}, | |
| {"word": "頻度", "count": 10}, | |
| ] | |
| result = save_summary(test_data, "test_summary.txt") | |
| print(f"Status: {result['status']}") | |
| print(f"Saved to: {result['path']}") | |
| # 保存されたファイルを読み込んで表示 | |
| print("\nSaved content:") | |
| with open(result['path'], 'r', encoding='utf-8') as f: | |
| print(f.read()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment