2026年1月22日木曜日

MCPツールshow_gui_object追加

 LLMがスケジュールナース内のオブジェクト操作を行ったとすると、それが、GUI上では、どうなるかを確認したくなると思います。LLMが「変更したけれどもこれでいいか?」みたいな状況を想定しています。

そこで、APIとMCPツールを追加しました。下がそのテストの様子です。

show_gui_objectは、二つの引数を持ちます。一つ目は、nameでこれは、

基本的には、スケジュールナース内オブジェクト固有の名前をセットします。この名前は、オブジェクト毎に決まっていて、ユーザが、変えることはできません。

例外は、制約グループ名です。複数のグループ名を区別する必要があるので、グループ名をセットします。

2番目は、def_nameで、ユーザが設定した名前が基本です。

@mcp.tool()
async def show_gui_object(name: str,def_name:str) ->ApiResponse:
    """(操作した)スケジュールナース内オブジェクトの状態に対応するGUI設定画面を前面に出すMCP ツール"""
    return await call_winforms_api("show_gui_object", {"text": name,"def_name":def_name})
  




        
async def test_show_gui_object():
        payload={"text":"ThisMonth","def_name":"今月"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"StartDisplayDate","def_name":"表示開始日"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"StartDate","def_name":"制約開始日"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"FinishDate","def_name":"制約終了日"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"HOLIDAYS","def_name":"祝"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"FRI","def_name":"金"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"AllDays","def_name":"全日"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"DayDef","def_name":"ICCRC非常勤土曜日勤務日"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()

        input()
        payload={"text":"DateAggregates","def_name":"制約開始日6日前から"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()

        payload={"text":"夜勤回数","def_name":"夜勤可能者平日の夜勤平準化"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()

        payload={"text":"管理職事務員看護師","def_name":"特定休みではない今月診療日処理"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()

        payload={"text":"Schedules","def_name":"スタッフ2184605"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()

        payload={"text":"StaffProperties","def_name":"スタッフ2184605"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"Solution","def_name":"スタッフ2184605"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"Shift Objects","def_name":"土日祝休み"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"ShiftAggregates","def_name":"夜勤"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        
        payload={"text":"GroupProperties","def_name":"Gr属性"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"GroupAggregates","def_name":"月曜休み者でない技看事"}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()
        payload={"text":"SolvingParameters","def_name":""}
        api_res= await call_winforms_api("show_gui_object",payload )
        input()

2026年1月21日水曜日

MCPリソースの実装について

MCPでもう一つ必要なMaterialは、TOOLに加えてリソース記述になります。


MCP(Model Context Protocol)サーバーを通じて、LLMがシステムの文脈(制約、予定、DSL)を正確に理解・操作するためのMarkdownで記述していきます。
リソースとは、何か?今一何を書けば良いか分かりませんでした。しかし、TOOLを書き上げた今、必要な情報であることは分かります。
スケジュールナースの内部オブジェクトを自由に操作できるTOOLは、公開しました。
足りないのは、そのTOOLの意味や目的、出来ること、出来ないこと、といった情報です。つまりスケジュールナース内部の構造を理解して、初めてTOOL群は意味をなすということです。
一言で言うなら、リソースとは、自然言語での指示を構造化データやPython DSLへ変換する際の人間とLLM間の「共通言語」としてのドキュメントではないでしょうか?それなしには、LLMはまともに動けないような、そういう資料がリソースである、というのが私の解釈です。

これをコパイに作成してもらおうとしたら、全く頓珍漢な答えが返ってきました。

「夜勤を連続 2 回以上禁止して」

Gemini → MCP:

{ "update_row_constraints": { "max_consecutive_night": 1 } }

「週末は必ず 2 名以上配置して」

Gemini → MCP:

{ "update_column_constraints": { "weekend_min_staff": 2 } }


これは、LLMがスケジュールナースシステムを理解していないが為に起こる齟齬だと考えます。そういう齟齬がないようにDocumentを整えることが、リソースの実装そのものです。

システムの根幹なので、AIに考えるように指示するのは筋違いで、設計者である私が書くべきも類なのだ、と悟りました。AIに教えるには、国語力が重要。



2026年1月20日火曜日

MCPサーバの実装

書いてみました。これは、スケジュールナースMCP TOOL実装になります。AIの力を借りて書きました。

スケジュールナースのシフト勤務表に関わるオブジェクトをLLMが自由に操作できるようにすることが目的です。タスクに関しては、シフト勤務表での知見が集積し、物になりそうなら、進めることとして、現時点では実装しません。

仮に期待に反し、LLMで全く動かなかったとしても、スケジュールナースをサーバとして使用できるようにするためのAPI機能を公開しているので、それなりに役に立つはずです。無駄にはなりません。

従来、サーバの設計者には、ソルバとのインタフェースのみを提供していました。しかし、これがあると、GUIで操作に加えて、プログラムでの操作も容易に出来るようになります。双方向の面から、ソルバインタフェースを操作出来るようになります。つまり、

■GUI⇒ソルバIF⇒ソルバ

■ソルバIF⇒GUI

これにより、ソルバインタフェースが、より分かり易くなる効果が期待できます。ソルバインタフェースを理解する学習教材としても機能するのではないか、と思います。



import asyncio
import json
from fastmcp import FastMCP
import asyncio



from fastmcp import Client # クライアントをインポート
import asyncio
import win32file
import win32pipe


import pywintypes

import uuid

from pydantic import BaseModel, Field
from datetime import datetime
from typing import List, Literal, Optional
from pydantic import BaseModel, Field, model_validator, field_validator

from typing import TypeVar, Type, Optional
from typing import Set
from pydantic import BaseModel, ValidationError

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp import Context

# 任意のBaseModelを指す型変数 T を定義
T = TypeVar("T", bound=BaseModel)


class PipeNotConnectedError(Exception):
    pass

class AsyncNamedPipeClient:
    def __init__(self, pipe_name: str):
        self.pipe_path = fr"\\.\pipe\{pipe_name}"
        self.handle = None

    async def connect(self, retry: int = 5, delay: float = 0.5):
        """パイプ接続。リトライ付き"""
        loop = asyncio.get_running_loop()

        def _connect():
            return win32file.CreateFile(
                self.pipe_path,
                win32file.GENERIC_READ | win32file.GENERIC_WRITE,
                0, None,
                win32file.OPEN_EXISTING,
                0, None
            )

        for attempt in range(retry):
            try:
                self.handle = await loop.run_in_executor(None, _connect)
                return
            except pywintypes.error as e:
                #print (e.winerror)
                # パイプが存在しない / 接続できない
                if e.winerror == 2:  # ERROR_FILE_NOT_FOUND
                    pass
                elif e.winerror == 231:  # ERROR_PIPE_BUSY
		    
                    pass
                else:
                    raise

            await asyncio.sleep(delay)

        raise PipeNotConnectedError(f"パイプ {self.pipe_path} に接続できませんでした。")

    async def write_line(self, text: str):
        if not self.handle:
            raise PipeNotConnectedError("パイプが接続されていません。")

        loop = asyncio.get_running_loop()
        data = (text + "\n").encode("utf-8")

        def _write():
            win32file.WriteFile(self.handle, data)

        try:
            await loop.run_in_executor(None, _write)
        except pywintypes.error as e:
            raise PipeNotConnectedError(f"パイプ書き込みエラー: {e}")

    async def read_line(self) -> str:
        if not self.handle:
            raise PipeNotConnectedError("パイプが接続されていません。")

        loop = asyncio.get_running_loop()

        def _read():
            return win32file.ReadFile(self.handle, 1024 * 1024 * 10)#20)#このサイズが小さいとBrokenPipeErrorが発生する Solutionの大きいInstanceのSolution読み込みに注意
        #await asyncio.sleep(1)
        while True:
            try:
                _, data = await loop.run_in_executor(None, _read)
            except pywintypes.error as e:
                if e.winerror == 109:  # Broken Pipe
                    print("Broken Pipe Detected. Retry from Connection")
                    #await asyncio.sleep(1)
                    #await self.connect()
                    #continue
                    raise PipeNotConnectedError(f"パイプ読み込みエラー: {e}")
                else:
                    raise PipeNotConnectedError(f"パイプ読み込みエラー: {e}")

            if not data:
                raise PipeNotConnectedError("パイプが切断されました。")

            return data.decode("utf-8").rstrip("\n")

    async def close(self):
        print("Closing Pipe")
        if self.handle:
            try:
                win32file.CloseHandle(self.handle)
            except Exception:
                pass
            self.handle = None

# date_objectの定義
class DateObject(BaseModel):
    name: str = Field(
        ...,
        description=(
            "オブジェクトの種別名。既定オブジェクトの場合は以下のいずれかを使用: "
            "'StartDate', 'StartDisplayDate', 'FinishDate', 'AllDays', 'ThisMonth', "
            "'SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'HOLIDAYS', 'SHOLIDAYS'。 "
            "自由な曜日定義を行う場合は 'DayDef' を使用してください。"
            """
	        "StartDate" 必須。 制約開始日となる。通常、'今月'の最初の日に設定する。通常先月部には制約せず'今月'だけ制約対象とする。ただし、連続勤務日数制約等で、先月部を参照する必要場合もある。それには、StartDisplayDateで対象日を設定する。つまり、StartDisplayDate<=StartDate<=FinishDateの関係となる。
	        "StartDisplayDate" 必須。 表示開始日、通常、StartDateの5日前に設定する。スケジュールナースが対象とする全てのDateの最初の日に設定する
	        "FinishDate" 必須。制約終了日、通常、'今月'の最後の日に設定する
	        "DayDef	ユーザ定義用
	        "AllDays" StartDateからFinishDateまでの全てのDayを表す曜日集合に自動設定される。ユーザはReadOnly
	        "ThisMonth" '今月'の定義となる。制約開始日から、制約終了日までに自動設定される。ユーザはReadOnly
	        "SUN" 日曜日ユーザはReadOnly
        	"MON" 月曜日ユーザはReadOnly
	        "TUE" 火曜日ユーザはReadOnly
	        "WED" 水曜日ユーザはReadOnly
	        "THU" 木曜日ユーザはReadOnly
	        "FRI" 金曜日ユーザはReadOnly
	        "SAT" 土曜日ユーザはReadOnly
	        "HOLIDAYS" 祝日ユーザはReadOnly
	        "SHOLIDAYS" 振替休日ユーザはReadOnly"""
        )
    )
    def_name: str = Field(
        ...,
        description="曜日名または定義名。空文字の場合は自動的に無効(use=false)として扱われます。"
    )
    dates: List[str]=Field(# datetime] = Field(
        default_factory=list,
        description="DateTimeのリスト。ISO8601形式(YYYY-MM-DD)で指定してください。"
    )
    use: bool = Field(
        default=True,
        description="有効(true)か無効(false)か。def_nameが空の場合は無効となります。"
    )
    predefined: bool = Field(
        default=False,
        description="既定オブジェクト(true)かユーザ定義(false)かを示すフラグ。trueの場合Readonlyとなる。"
    )
    val: int = Field(
        default=0,
        description="現在は使用されていません(互換性のために維持)。"
    )

    # C#のコンストラクタロジック(def_nameが空ならuse=false)を再現
    @field_validator("use", mode="before")
    @classmethod
    def validate_use_by_def_name(cls, v, info):
        # def_nameが提供されているか確認し、空なら強制的にFalseにする
        def_name = info.data.get("def_name", "")
        if not def_name or len(def_name.strip()) == 0:
            return False
        return v
# C# の date_objects クラスに対応するモデル
class DateObjects(BaseModel):
    """
    date_object のリストを保持するコンテナクラスです。
    """
    members: List[DateObject] = Field(
        default_factory=list, 
        description="date_object のリスト"
    )
    
    days: int = Field(
        default=0, 
        description="現在使用していないフィールド(互換性のため維持)"
    )
    
    target_year: int = Field(
        default=0, 
        description="現在使用していないフィールド(互換性のため維持)"
    )
    
    target_month: int = Field(
        default=0, 
        description="現在使用していないフィールド(互換性のため維持)"
    )

class ShiftObject(BaseModel):
    """
    シフト定義を管理するクラスです。
    """
    # C# の public const string name に相当
    name: str = Field(default="ShiftDef", frozen=True)
    
    # 基本フィールド
    use: bool = Field(default=False, description="シフト定義が有効無効を示すフラグ")
    def_name: str = Field(default="", description="シフト名")
    auto_schedule: bool = Field(default=True, description="自動スケジュールのときtrue")
    color: str = Field(default="Gray", description="GUI色")
    label: str = Field(default="", description="ラベル名")
    
    # リスト型フィールド (default_another_labels = 2 を再現)
    another_labels: List[str] = Field(
        default_factory=lambda: [""] * 2, 
        description="別名ラベルのリスト"
    )
    another_colors: List[str] = Field(
        default_factory=lambda: ["Gray"] * 2, 
        description="ラベルの色リスト"
    )
    
    num_cnt: int = Field(default=0)
    work_start_time: str = Field(default="", description="勤務開始時間")
    work_hours: int = Field(default=-1, description="勤務時間定義(15分単位、-1で無定義)")

    @model_validator(mode='before')
    @classmethod
    def validate_logic(cls, data: dict):
        """
        C#のコンストラクタにあるロジックを再現します。
        - def_name が空なら use を False にする
        - 文字列のトリム処理
        """
        if isinstance(data, dict):
            # def_name のトリム処理
            def_name = data.get("def_name", "")
            if isinstance(def_name, str):
                def_name = def_name.strip()
                data["def_name"] = def_name
                
                # use の論理: def_name が空なら use は強制的に False
                if not def_name:
                    data["use"] = False

            # label のトリム処理
            label = data.get("label", "")
            if isinstance(label, str):
                data["label"] = label.strip()

        return data

    class ConfigDict:
        # C#の命名規則(スネークケース)を維持しつつ、
        # 柔軟な入力を許可する場合の設定
        populate_by_name = True

class ShiftObjects(BaseModel):
    """
    shift_objectのリストを管理するクラスです。
    """
    # C# の public const string name = "Shift Objects" に相当
    name: str = Field(
        default="Shift Objects", 
        frozen=True, 
        description="オブジェクト名"
    )

    # C# の public List members に相当
    members: List[ShiftObject] = Field(
        default_factory=list, 
        description="shift_objectのリスト"
    )

    class ConfigDict:
        # JSONのキーが "members" であれば、自動的に List[ShiftObject] にデシリアライズされます
        populate_by_name = True
# 1. C#のクラス構造をPydanticモデルで定義
class ShiftAggregate(BaseModel):
    use: bool = Field(
        default=False, 
        description="有効(true)か無効かを示すフラグ。無効ならソルバは参照しない"
    )
    # const扱いの名前。LLMが変更しないようdefault固定にする
    name: str = Field(
        default="ShiftAggregateDef", 
        frozen=True,
        description="オブジェクト名(固定値: ShiftAggregateDef)"
    )
    def_name: str = Field(
        default="", 
        description="定義名。シフトオブジェクトのORまたはNOTの集合体を表す一意識別子"
    )
    color: str = Field(
        default="Grey", 
        description="ラベル背景色(例: Blue, #FF0000)"
    )
    # Operatorをリテラルで制限
    Operator: Literal["OR", "NOT","OR_INCLUDE_NON_AUTO"] = Field(
        default="OR", 
        description="演算子。OR(いずれか)またはNOT(以外)のみ選択可能"
    )
    members: List[str] = Field(
        default_factory=list, 
        description="シフトオブジェクト名のリスト(例: ['日勤', '夜勤'])"
    )
    label: str = Field(
        default="", 
        description="GUI(予定入力・解上)での表示名"
    )
class ShiftAggregates(BaseModel):
    """
    ShiftAggregateのリストを管理するクラスです。
    """
    # C# の public const string name = "Shift Objects" に相当
    name: str = Field(
        default="ShiftAggregates", 
        frozen=True, 
        description="オブジェクト名"
    )

    # C# の public List members に相当
    members: List[ShiftAggregate] = Field(
        default_factory=list, 
        description="ShiftAggregateのリスト"
    )

    class ConfigDict:
        # JSONのキーが "members" であれば、自動的に List[ShiftObject] にデシリアライズされます
        populate_by_name = True

class DateAggregate(BaseModel):
    """
    date_objectまたは他のdate_aggregateを組み合わせて、新しい曜日集合を定義します。
    """
    
    # C#のconst string name = "DateAggregate" を再現
    name: str = Field(
        default="DateAggregate",
        frozen=True,
        description="オブジェクト名。常に 'DateAggregate' 固定です。"
    )

    Operator: str = Field(
        default="OR",
        description="演算子。AND(かつ)、OR(または)、NOT(以外)-10, -9,-8..-1 ,+1,+2,..+10,+1,Rep2,Rep3,Rep4,Rep7,Rep14,等のいずれかを指定します。"
    )

    def_name: str = Field(
        ...,
        description="この曜日集合の定義名(例: '平日', '特定日集合')。一意である必要があります。"
    )

    dates: List[str] = Field(
        default_factory=list,
        description=(
            "対象となる曜日メンバー名のリスト。date_object または "
            "他の date_aggregate の def_name を指定します。循環参照は禁止です。"
        )
    )

    use: bool = Field(
        default=False,
        description="有効(true)か無効(false)か。無効な場合、ソルバはこの定義を無視します。"
    )

# C# の date_aggregates クラスに対応するモデル
class DateAggregates(BaseModel):
    """
    複数の日付集合(DateAggregate)を管理するリストクラスです。
    """
    # C# の const string name = "DateAggregates" を再現
    name: str = Field(
        default="DateAggregates",
        frozen=True,
        description="オブジェクト名。常に 'DateAggregates' 固定です。"
    )

    # List members を再現
    members: List[DateAggregate] = Field(
        default_factory=list,
        description="DateAggregate オブジェクトのリスト"
    )
class GroupProperty(BaseModel):
    """
    スタッフの属性項目(例:夜勤属性)とその選択肢リストを定義します。
    """
    use: bool = Field(
        default=False, 
        description="有効(true)か無効かを示すフラグ。無効ならソルバは参照しません。"
    )
    
    # JAN112026 更新: オブジェクト名固定
    name: str = Field(
        default="GroupPropertyDef",
        frozen=True,
        description="オブジェクト名。常に 'GroupPropertyDef' 固定です。"
    )
    
    def_name: str = Field(
        default="",
        description="スタッフプロパティアイテム名(例:'夜勤属性'、'役職' など)。"
    )
    
    # C#コンストラクタの「空文字7つ」の初期化を再現
    members: List[str] = Field(
        default_factory=lambda: [""] * 7,
        description="属性の選択肢リスト(例:['夜勤可', '夜勤不可', '', ... ])。"
    )

class GroupProperties(BaseModel):
    """
    複数のスタッフ属性定義(GroupProperty)を管理するリストクラスです。
    """
    name: str = Field(
        default="GroupProperties",
        frozen=True,
        description="オブジェクト名。常に 'GroupProperties' 固定です。"
    )
    
    members: List[GroupProperty] = Field(
        default_factory=list,
        description="GroupProperty オブジェクトのリストです。"
    )
class ShiftLevel(BaseModel):
    """
    予定セルにおけるシフトとその制約レベル、表示用インデックスを定義します。
    """
    shift: str = Field(
        default="",
        description="shift_object または shift_aggregate_class の定義名(def_name)を参照します。"
    )
    
    level: int = Field(
        default=0,
        description=(
            "ソフト制約の重みインデックス(通常1-7)。"
            "0以下の値はハード制約(絶対に守るべき制約)を意味します。"
        )
    )
    
    a_ix: int = Field(
        default=-1,
        description=(
            "another_labelを指すインデックス。デフォルトは-1。"
            "0以上のとき、該当するラベルのインデックスとして機能します。"
        )
    )
    
    locked: bool = Field(
        default=False,
        description="GUI上で予定セルの編集がロックされているかどうかを示すフラグ(2026年1月12日追加)。"
    )

class GroupAggregate(BaseModel):
    """
    スタッフ属性(GroupProperty)または他の集合(GroupAggregate)を組み合わせて、
    新しい属性集合を定義します。
    """
    use: bool = Field(
        default=False, 
        description="有効(true)か無効かを示すフラグ。無効ならソルバは参照しません。"
    )

    name: str = Field(
        default="GroupAggregateDef",
        frozen=True,
        description="オブジェクト名。常に 'GroupAggregateDef' 固定です。"
    )

    def_name: str = Field(
        default="",
        description="この属性集合の定義名(例: '夜勤リーダー候補', '新人以外' など)。"
    )

    Operator: Literal["AND", "OR", "NOT"] = Field(
        default="OR",
        description="演算子。AND(かつ)、OR(または)、NOT(以外)のいずれかを指定します。"
    )

    # C#コンストラクタの「空文字7つ」の初期化を再現
    members: List[str] = Field(
        default_factory=lambda: [""] * 7,
        description=(
            "対象となるメンバー名のリスト。group_property または "
            "他の group_aggregate の def_name を参照します。循環参照は禁止です。"
        )
    )

class GroupAggregates(BaseModel):
    """
    複数のスタッフ属性集合定義(GroupAggregate)を管理するリストクラスです。
    """
    name: str = Field(
        default="GroupAggregates",
        frozen=True,
        description="オブジェクト名。常に 'GroupAggregates' 固定です。"
    )
    
    members: List[GroupAggregate] = Field(
        default_factory=list,
        description="GroupAggregate オブジェクトのリストです。"
    )
class StaffProperty(BaseModel):
    """
    個々のスタッフの属性(可能なシフト、タスク、所属グループ等)を定義します。
    """
    use: bool = Field(
        default=True,
        description="有効(true)か無効かを示すフラグ。無効ならソルバは参照しません。"
    )

    name: str = Field(
        default="StaffPropertyDef",
        frozen=True,
        description="オブジェクト名。常に 'StaffPropertyDef' 固定です。"
    )

    def_name: str = Field(
        default="",
        description="スタッフの名前(一意の識別子)。"
    )

    shifts: Set[str] = Field(
        default_factory=list,
        description="このスタッフが担当可能なシフト名のリスト(C#のSortedSetに対応)。"
    )

    tasks: Set[str] = Field(
        default_factory=list,
        description="このスタッフが担当可能なタスク名のリスト(C#のSortedSetに対応)。"
    )

    properties: dict[str, str] = Field(
        default_factory=dict,
        description="グループプロパティ名と選択された属性値のマップ(例: {'夜勤属性': '夜勤可'})。"
    )

    comment: str = Field(
        default="",
        description="スタッフに関するメモ・コメント(2026年1月12日更新)。"
    )

class StaffProperties(BaseModel):
    """
    全スタッフの属性定義を一括管理するコンテナクラスです。
    """
    name: str = Field(
        default="StaffProperties",
        frozen=True,
        description="オブジェクト名。常に 'StaffProperties' 固定です。"
    )
    
    members: List[StaffProperty] = Field(
        default_factory=list,
        description="StaffProperty オブジェクトのリストです。"
    )

class ShiftSchedules(BaseModel):
    """
    全スタッフ・全日程のシフト割り当て(二次元リスト)を管理します。
    """
    # C# の const string name = "Schedules" を再現
    name: str = Field(
        default="Schedules",
        frozen=True,
        description="オブジェクト名。常に 'Schedules' 固定です。"
    )

    # List> members を再現
    # 外側のリストが「スタッフ」、内側のリストが「日付」に対応します
    members: list[list[ShiftLevel]] = Field(
        default_factory=list,
        description="シフト割り当ての二次元リスト。例: members[スタッフIndex][日付Index]"
    )

class Solution(BaseModel):
    """
    全スタッフ・全日程のシフト割り当て(二次元リスト)結果を格納するコンテナ。
    """
  
    # List> members を再現
    # 外側のリストが「スタッフ」、内側のリストが「日付」に対応します
    members: list[list[ShiftLevel]] = Field(
        default_factory=list,
        description="シフト割り当ての二次元リスト。例: members[スタッフIndex][日付Index]"
    )

class RowConstraint(BaseModel):
    """
    行制約(勤務パターンの禁止、回数、勤務時間など)を定義する詳細モデルです。
    """
    # 基本情報
    name: str = Field(default="RowConstraint", frozen=True)
    def_name: str = Field(..., description="行制約名(必須)。")
    use: bool = Field(default=True, description="有効フラグ。def_nameが空の場合はFalseになります。")
    phase_mode: bool = Field(default=False, description="フェーズモードフラグ。")
    equal_count_mode: bool = Field(default=False, description="同数カウントモードフラグ。")

    # 日付・スタッフ属性
    primary_day_type: str = Field(default="", description="date_object または date_aggregate の名称。必須")
    first_day_type: str = Field(default="", description="パターンの開始日条件。")
    last_day_type: str = Field(default="", description="パターンの終了日条件。")
    staff_property_type: str = Field(default="", description="対象となるスタッフ属性。")

    # パターン定義
    pattern: list[ShiftLevel] = Field(default_factory=list, description="シフトパターンのリスト。必須")
    inv_pattern: list[bool] = Field(default_factory=list, description="patternに対応するNOTリスト。必須")
    shift_pattern: list[ShiftLevel] = Field(default_factory=list, description="phase mode用シフトパターン。")
    shift_inv_pattern: list[bool] = Field(default_factory=list, description="phase mode用NOTリスト。")

    # 同数カウントモード用
    intercept: int = Field(default=0, description="数値オフセット。")
    A_daytypes: list[str] = Field(default_factory=list, description="Aグループの曜日タイプ。")
    B_daytypes: list[str] = Field(default_factory=list, description="Bグループの曜日タイプ。")

    # 制約の詳細設定
    constraint_type: int = Field(default=0, description="0:シフト禁止, 1:回数, 2:勤務時間, 3:Phase, 4:回数比較 必須")
    max_value: int = Field(default=-1)
    min_value: int = Field(default=-1)
    max_double: float = Field(default=-1.0)
    min_double: float = Field(default=-1.0)
    max_staff_property: str = Field(default="")
    min_staff_property: str = Field(default="")
    soft_constraint_level: int = Field(default=0, description="0:ハード, 1-7:ソフトレベル 必須")

    # バリデータ: C#コンストラクタのロジックを再現
    @field_validator("def_name", mode="before")
    @classmethod
    def trim_name(cls, v: str) -> str:
        return v.strip() if isinstance(v, str) else v

    @model_validator(mode="after")
    def apply_constructor_logic(self) -> "RowConstraint":
        # def_nameが空なら自動的に無効化
        if not self.def_name:
            self.use = False
        
        # equal_count_mode が True の場合の初期設定
        if self.equal_count_mode:
            self.constraint_type = 4
            self.intercept = 0
            if not self.A_daytypes:
                self.A_daytypes = [""]
            if not self.B_daytypes:
                self.B_daytypes = [""]
        
        return self

class RowConstraintsGroup(BaseModel):
    """
    行制約のグループ定義です。
    C#のコンストラクタ row_constraints_group(def_name_, phase_mode_, equal_count_mode_) の仕様を反映しています。
    """
    def_name: str = Field(
        ..., 
        description="制約グループの定義名(必須)。"
    )
    
    phase_mode: bool = Field(
        default=False,
        description="フェーズモード。シフト勤務表では常にFalse、タスク勤務表ではGUI制御に使用します。"
    )
    
    equal_count_mode: bool = Field(
        default=False,
        description="回数比較モード(2023年3月追加)。オンにすると回数比較専用のGUIになります。"
    )

    name: str = Field(
        default="RowConstraintsGroup",
        description="オブジェクト名"
    )

    use: bool = Field(
        default=True,
        description="有効(true)か無効か。無効ならソルバは参照しません。"
    )

    members: list[RowConstraint] = Field(
        default_factory=list,
        description="このグループに属する個別の行制約(RowConstraint)のリスト"
    )

    @field_validator("def_name", mode="before")
    @classmethod
    def trim_def_name(cls, v: str) -> str:
        """C#のTrim()処理を再現"""
        if isinstance(v, str):
            return v.strip()
        return v

class RowConstraints(BaseModel):
    """
    全ての行制約グループを一括管理するクラスです。
    """
    name: str = Field(
        default="RowConstraints",
        frozen=True,
        description="オブジェクト名。常に 'RowConstraints' 固定です。"
    )

    members: list[RowConstraintsGroup] = Field(
        default_factory=list,
        description="行制約グループ (RowConstraintsGroup) のリスト"
    )

class ColumnConstraint(BaseModel):
    """
    列制約(日付ごとの必要人数、勤務時間、シフト禁止・強制など)を定義するモデルです。
    """
    # 基本情報
    name: str = Field(default="ColumnConstraint", frozen=True)
    def_name: str = Field(..., description="列制約名(必須)。前後の空白は自動的に削除されます。")
    use: bool = Field(default=True, description="有効フラグ。名前が空の場合は自動的にFalseになります。")

    # シフト・タスク・日付・スタッフ属性
    shift_type: str = Field(default="", description="shift_object または shift_aggregate の名称。")
    shift_at_phase: str = Field(default="", description="タスク勤務表フェーズ用シフト(2023年1月4日追加)。")
    task_type: str = Field(default="", description="タスクオブジェクトまたはタスク集合の名称(タスク勤務表用)。")
    primary_day_type: str = Field(default="", description="date_object または date_aggregate の名称。")
    staff_property_type: str = Field(default="", description="スタッフ属性(group_property 等)の名称。")

    # 制約タイプ
    constraint_type: int = Field(
        default=0, 
        description="0:シフト禁止, 1:max-min, 2:シフト強制, 3:勤務時間max-min, 4:整数計数"
    )

    # タスク勤務表用 Daily制約テーブルプロパティ
    dayphase_vec_max: str = Field(default="", description="Daily制約テーブル(Max)に対応するプロパティ。")
    dayphase_vec_min: str = Field(default="", description="Daily制約テーブル(Min)に対応するプロパティ。")

    # 数値制約(整数・実数)
    max_value: int = Field(default=-1, description="max-minの場合の最大値。設定しない場合は負の値。")
    min_value: int = Field(default=-1, description="max-minの場合の最小値。設定しない場合は負の値。")
    max_double: float = Field(default=-1.0, description="勤務時間max。設定しない場合は負の値。")
    min_double: float = Field(default=-1.0, description="勤務時間min。設定しない場合は負の値。")

    # ソフト制約レベル
    soft_constraint_level: int = Field(
        default=0, 
        description="ソフト制約レベル。0:ハード, 1:最弱, 7:最強。"
    )
    soft_constraint_level_max: int = Field(
        default=0, 
        description="Max専用のソフト制約レベル。min-maxでレベルを分けたい場合に使用。"
    )

    # バリデータ: C#コンストラクタのロジックを再現
    @field_validator("def_name", mode="before")
    @classmethod
    def trim_def_name(cls, v: str) -> str:
        """C#のTrim()を再現"""
        return v.strip() if isinstance(v, str) else v

    @model_validator(mode="after")
    def validate_use_state(self) -> "ColumnConstraint":
        """名前が空なら有効フラグをFalseにするロジックを再現"""
        if not self.def_name:
            self.use = False
        return self

class ColumnConstraintsGroup(BaseModel):
    """
    列制約のグループ定義です。
    C#のコンストラクタ column_constraints_group(def_name_, phase_mode_) の仕様を反映しています。
    """
    def_name: str = Field(
        ..., 
        description="制約グループの定義名(必須)。"
    )
    
    phase_mode: bool = Field(
        default=False,
        description="フェーズモード。シフト勤務表では常にFalse、タスク勤務表ではGUI制御に使用します。"
    )

    name: str = Field(
        default="ColumnConstraintsGroup",
        frozen=True,
        description="オブジェクト名"
    )

    use: bool = Field(
        default=True,
        description="有効(true)か無効か。def_nameが空文字の場合はFalseとして扱われます。"
    )

    members: list[ColumnConstraint] = Field(
        default_factory=list,
        description="このグループに属する個別の列制約(ColumnConstraint)のリスト"
    )

    # Color型はSystem.Text.Jsonで直接扱えないため、文字列(HTMLカラーコード)として定義
    #tab_color: str = Field(
    #    default="#FFFFFF", 
    #    description="GUIのタブカラー。HTMLカラーコード(例: #FFFFFF)または色名(White)で指定。"
    #)

    @field_validator("def_name", mode="before")
    @classmethod
    def validate_and_trim_def_name(cls, v: str) -> str:
        """C#のTrim()処理および空文字時のuse=falseロジックの再現"""
        if isinstance(v, str):
            trimmed = v.strip()
            return trimmed
        return v

    @field_validator("use", mode="after")
    @classmethod
    def set_use_by_def_name(cls, v: bool, info) -> bool:
        """def_nameが空なら強制的にuseをFalseにするロジックを再現"""
        if not info.data.get("def_name"):
            return False
        return v

class ColumnConstraints(BaseModel):
    """
    全ての列制約グループを一括管理するクラスです。
    """
    name: str = Field(
        default="ColumnConstraints",
        frozen=True,
        description="オブジェクト名。常に 'ColumnConstraints' 固定です。"
    )

    members: list[ColumnConstraintsGroup] = Field(
        default_factory=list,
        description="列制約グループ (ColumnConstraintsGroup) のリスト"
    )
class PairConstraint(BaseModel):
    """
    2つの要素(AとB)の間の相関制約(禁止、ならば、比較など)を定義するモデルです。
    """
    name: str = Field(default="PairConstraint", frozen=True)
    def_name: str = Field(default="", description="制約名(一意)")
    use: bool = Field(default=True, description="有効フラグ")
    
    # モード設定
    phase_mode: bool = Field(default=False, description="タスク勤務表モードフラグ")
    equal_count_mode: bool = Field(default=False, description="リニア不等式(数値比較)モードフラグ")

    # 対象 A
    shift_typeA: str = Field(default="", description="Aのシフト/タスク集合名")
    staff_property_typeA: str = Field(default="", description="Aのスタッフ属性/集合名")
    operatorA: str = Field(default="", description="通常時: OR, AND等 / 数値比較時: A係数(数字またはBFULL)")

    # 対象 B
    shift_typeB: str = Field(default="", description="Bのシフト/タスク集合名")
    staff_property_typeB: str = Field(default="", description="Bのスタッフ属性/集合名")
    operatorB: str = Field(default="", description="通常時: OR, AND等 / 数値比較時: B係数(数字またはAFULL)")

    # 共通条件
    day_type: str = Field(default="", description="対象となる日付集合名")
    dayoffset: int = Field(default=0, description="Aに対するBの日付オフセット")

    # 制約の詳細
    constraint_type: int = Field(
        default=0, 
        description="0:!(A&&B)禁止, 1:AならばB, 2:A==B, 3:A>=B, 4:A<=B"
    )
    soft_constraint_level: int = Field(default=0, description="0:ハード, 1-7:ソフトレベル")

    # タスク・フェーズ関連 (2024年1月更新対応)
    phase_str: str = Field(default="", description="フェーズ識別子 (例: ph0)")
    shift_typeA_at_phase: str = Field(default="", description="フェーズ時のAシフト参照")
    shift_typeB_at_phase: str = Field(default="", description="フェーズ時のBシフト参照")
    
    # 数値オフセット関連
    numerical_offset: int = Field(default=0, description="数値比較モード時の定数オフセット")
    numerical_offset_string: str = Field(default="", description="数値オフセットの文字列表現 (2024年1月追加)")

class PairConstraintsGroup(BaseModel):
    """
    ペア制約のグループ。GUIのタブ表示単位に対応します。
    """
    name: str = Field(default="PairConstraintsGroup", frozen=True)
    def_name: str = Field(default="", description="グループ名")
    use: bool = Field(default=True)
    equal_count_mode: bool = Field(default=False)
    phase_mode: bool = Field(default=False)
    members: list[PairConstraint] = Field(default_factory=list)
    
    # System.Drawing.Colorの互換性のため文字列として保持
    #tab_color: str = Field(default="White", description="タブの背景色 (ColorTranslator形式)")

class PairConstraints(BaseModel):
    """
    全てのペア制約グループを管理するトップレベルクラスです。
    """
    name: str = Field(default="PairConstraints", frozen=True)
    members: list[PairConstraintsGroup] = Field(default_factory=list)

    @field_validator("members", mode="before")
    @classmethod
    def ensure_list(cls, v):
        return v if isinstance(v, list) else []

class SwInt(BaseModel):
    """
    ソルバの制約許容数や重み(ウェイト)を定義するモデルです。
    """
    use: bool = Field(
        default=False, 
        description="有効(true)か無効(false)かを示すフラグ。"
    )
    
    value: int = Field(
        default=0, 
        description="1制約あたりのCARDINALS許容エラー数(スラック変数)。"
    )
    
    total_max_errors: int = Field(
        default=0, 
        description="現在使用されていません(0: 最小化を意味します)。"
    )
    
    weight: int = Field(
        default=1, 
        description=(
            "ソフト制約の重み(自然数、1-10推奨)。"
            "※ソフトレベル(1-7のインデックス)とは異なる「実際の重み係数」です。"
        )
    )
 
class SolvingParameters(BaseModel):
    """
    ソルバの求解パラメータ、ソフト制約レベルごとの重み設定、
    および外部Python制約などを管理するクラスです。
    """
    name: str = Field(
        default="SolvingParameters",
        frozen=True,
        description="オブジェクト名。常に 'SolvingParameters' 固定です。"
    )

    # Dictionary> solving_map を再現
    # 外側のキー(int)はソフト制約レベル(1-7など)、内側のキー(str)は制約名
    solving_map: dict[int, dict[str, SwInt]] = Field(
        default_factory=dict,
        description="ソフトレベルおよび制約名ごとの詳細設定マップ。"
    )

    # Pythonによる制約記述
    external_constraint_python: str = Field(
        default="",
        description="Pythonスクリプトによる外部制約記述。"
    )

    # プロジェクトコメント
    comment: str = Field(
        default="",
        description="プロジェクトの変更履歴やメモ用コメント。"
    )

    # 求解パラメータ Dictionary
    parameters: dict[str, int] = Field(
        default_factory=dict,
        description="ソルバの各種数値パラメータ(イテレーション回数等)。"
    )

    # 現在使用されていない、または内部用のフィールド
    aws_parameters: dict[str, str] = Field(
        default_factory=dict,
        description="AWS関連パラメータ(現在未使用)。"
    )
    external_constraint: str = Field(
        default="",
        description="言語制約記述(現在未使用)。"
    )
    python_property_file: str = Field(
        default="",
        description="内部用プロパティファイルパス(編集不可)。"
    )
    python_property_file_post: str = Field(
        default="",
        description="内部用プロパティファイルパス(後処理、編集不可)。"
    )

class ApiResponse(BaseModel):
    """
    WinForms API からの実行結果レスポンスを定義するモデルです。
    """
    # status の選択肢を Literal で定義(complete, rejected, accepted, error, unknown_api)
    status: Literal["complete", "rejected", "accepted", "error", "unknown_api"] = Field(
        ..., 
        description="APIリクエストの処理ステータス。"
    )
    
    solver_status: Optional[str] = Field(
        default=None, 
        description="ソルバの内部状態(例: Optimal, Feasible, Timeout 等)。"
    )
    
    error_reason: Optional[str] = Field(
        default=None, 
        description="エラーが発生した場合の理由、または詳細メッセージ。"
    )

    value: Optional[str] = Field(
        default=None, 
        description="APIから返される主要な戻り値(JSON文字列や特定の識別子など)。"
    )

    progress: int = Field(
        default=0, 
        description="現在の処理進捗状況。"
    )

    total: int = Field(
        default=100, 
        description="全体の処理数。デフォルト値は100です。"
    )

    def get_value_as(self, model_class: Type[T]) -> Optional[T]:
        if not self.value:
            return None
        try:
            return model_class.model_validate_json(self.value)
        except ValidationError:
            return None

#PIPE_NAME =f'\\\\.\\pipe\\winforms_pipe'# r'\\.\pipe\winforms_pipe'
PIPE_NAME = "winforms_pipe"

async def call_winforms_api(api_name: str, payload: dict, ctx=None)->ApiResponse:
    pipe = AsyncNamedPipeClient(PIPE_NAME)

    # 1. パイプ接続
    try:
        await pipe.connect()
    except PipeNotConnectedError as e:
        return {"status": "pipe_error", "message": str(e)}

    # 2. JSON リクエスト送信
    request = json.dumps({"api": api_name, "payload": payload})
    try:
        print("Writing api_name=",api_name)
        await pipe.write_line(request)
    except PipeNotConnectedError as e:

        await pipe.close()
        return {"status": "write_error", "message": str(e)}

    # 3. 継続的な読み取りループ
    try:
        print("Reading api_name=",api_name)

        while True:
            response_line = await pipe.read_line()
            if not response_line:
                break

            try:
                data = json.loads(response_line)
            except json.JSONDecodeError:
                continue # 不正な行はスキップして次を待つ

            status = data.get("status")
            solver_status = data.get("solver_status")
            error_reason=data.get("error_reason")
            value=data.get("value")
            print(status)
            if solver_status == "Feasible_Solution":
                print("実行可能解が見つかりました!")
          
            elif solver_status == "Solving":
       # 進捗報告時に詳細ステータスを添える
                if ctx:
                    await ctx.report_progress(progress=data.get("progress"), total=100)
                    print(f"ステータス: {solver_status}")
            # A. 完了時の処理
            if status == "complete":
                if value is not None :
                    print("value len=",len(value))
                # 最後に100%を報告(任意)
                if ctx:
                    await ctx.report_progress(progress=100, total=100)
                # 辞書を ApiResponse モデルに変換して返す
                return ApiResponse.model_validate(data)

		

            # B. エラー時の処理
            if status in ["error", "rejected", "unknown_api"]:#if status == "error" or status=="rejected" :

                if status=="unknown_api":
                    print("サポートしていないAPIです。")
                if status=="rejected":
                    print("APIが拒否されました")
                if solver_status=="Infeasible_Solution":
                    print("解が見つかりませんでした(実行不能)。")
                elif solver_status=="ProjectFile_NOT_Specified":
                    print("プロジェクトファイルが保存されていません。スケジュールナース→ファイルメニュー→名前をを付けて保存してください。")
                elif solver_status=="Compile_Failed":
                    print("プロジェクトファイルのコンパイルに失敗しました。")
                elif solver_status=="Solving_Engine_Invoking_Failure":
                    print("求解エンジンの起動に失敗しました。")
                elif solver_status=="Solving_Killed_by_User":
                    print("求解がユーザにより中止させられました")
                return  ApiResponse.model_validate(data)#辞書を ApiResponse モデルに変換して返す

            # C. それ以外(進捗報告など)
            if ctx:
                # WinForms側のJSONに progress/total があればそれを使う
                # 無ければ現在のステップとして報告
                p = data.get("progress", 0)
                t = data.get("total", 100)
                
                # Contextを通じてMCPクライアントに通知
                await ctx.report_progress(progress=p, total=t)
                
                # ログ出力(任意)
                # print(f"進捗報告中: {p}/{t}")

    except PipeNotConnectedError as e:
        return {"status": "read_error", "message": str(e)}
    #finally:
    #    await pipe.close()

    return {"status": "unexpected_termination"}

# -------------------------------
# WinForms に API を発行する関数
# -------------------------------





# FastMCP サーバー作成
mcp = FastMCP("local-mcp")
#mcp = FastMCP(name="私の最初のMCPサーバー")


# -------------------------------
# MCP ツール → WinForms API 呼び出し
# -------------------------------



@mcp.tool()
async def get_shift_definitions()-> ShiftObjects| None:
    """
    スケジュールナースから現在の全てのシフト定義(名前、色、ラベル、サブラベル等)を取得します。
    """
    api_res= await call_winforms_api("get_shift_objects", {} )
    #print("api_res=",api_res)
    if api_res.status=="complete":
        SO=api_res.get_value_as(ShiftObjects)
        #print("SO=",SO)
        return SO
    else:
        return None


@mcp.tool()
async def update_shift_definitions(data:ShiftObjects)-> ApiResponse:
    """
    スケジュールナースから現在の全てのシフト定義(名前、色、ラベル、サブラベル等)を取得します。
    """
    text = data.model_dump_json()
    payload={"text":text}

    return  await call_winforms_api("update_shift_objects",payload )
    
@mcp.tool()
async def get_shift_aggregates_definitions()-> ShiftAggregates | None:
    """
    スケジュールナースから現在の全てのシフト集合定義(名前、色、ラベル、サブラベル等)を取得します。
    """
    api_res= await call_winforms_api("get_shift_aggreggates", {} )
    
    if api_res.status=="complete":
        SA=api_res.get_value_as(ShiftAggregates)
        #print("SO=",SO)
        return SA
    else:
        return None

@mcp.tool()
async def get_shift_solution()-> Solution | None:
    """
    スケジュールナースから現在の全てのシフト集合定義(名前、色、ラベル、サブラベル等)を取得します。戻り値は、Schedulesクラスと同じ2次元形式です。
    """
    api_res= await call_winforms_api("get_shift_solution", {} )
    if api_res.status=="complete":
        So=api_res.get_value_as(Solution)
        #print("SO=",SO)
        return So
    else:
        return None

@mcp.tool()
async def get_date_objects()-> DateObjects | None:
    """
    スケジュールナースから現在の全てのDateObjectを取得します。
    """
    api_res= await call_winforms_api("get_date_objects", {} )
    if api_res.status=="complete":
        So=api_res.get_value_as(DateObjects)
        #print("SO=",SO)
        return So
    else:
        return None

@mcp.tool()
async def get_date_aggregates()-> DateAggregates | None:
    """
    スケジュールナースから現在の全てのDateAggregateを取得します。
    """
    api_res=await call_winforms_api("get_date_aggregates", {} )
    if api_res.status=="complete":
        #print("api_res=",api_res.value)
        So=api_res.get_value_as(DateAggregates)
        #print("SO=",SO)
        return So
    else:
        return None

@mcp.tool()
async def get_group_properties()-> GroupProperties | None:
    """
    スケジュールナースから現在の全てのGroupPropertyを取得します。
    """
    api_res=await call_winforms_api("get_group_properties", {} )
    if api_res.status=="complete":
        Gp=api_res.get_value_as(GroupProperties)
        return Gp
    else:
        return None

@mcp.tool()
async def get_group_aggregates()-> GroupAggregates| None:
    """
    スケジュールナースから現在の全てのGroupAggregateを取得します。
    """
    api_res= await call_winforms_api("get_group_aggregates", {} )
    if api_res.status=="complete":
        #print("api_res=",api_res)
        Gp=api_res.get_value_as(GroupAggregates)
        return Gp
    else:
        return None

@mcp.tool()
async def get_staff_properties()-> StaffProperties:
    """
    スケジュールナースから現在の全てのStaffPropertyを取得します。
    """
    api_res= await call_winforms_api("get_staff_properties", {} )
    if api_res.status=="complete":
        #print("api_res=",api_res)
        Sp=api_res.get_value_as(StaffProperties)
        return Sp
    else:
        return None

@mcp.tool()
async def get_shift_schedules()->ShiftSchedules| None:
    """
    スケジュールナースから現在のSchedulesを取得します。
    """
    api_res= await call_winforms_api("get_shift_schedules", {} )
    if api_res.status=="complete":
        #print("api_res=",api_res)
        Sc=api_res.get_value_as(ShiftSchedules)
        return Sc
    else:
        return None

@mcp.tool()
async def get_row_constraints()-> RowConstraints| None:
    """
    スケジュールナースから現在の全てのRowConstraintを取得します。
    """
    api_res=await call_winforms_api("get_row_constraints", {} )
    #print("api_res=",api_res)
    
    if api_res.status=="complete":
        #print("api_res=",api_res)
        Sc=api_res.get_value_as(RowConstraints)
        return Sc
    else:
        return None
   
@mcp.tool()
async def get_column_constraints()-> ColumnConstraints|None:
    """
    スケジュールナースから現在の全てのColumnConstraintを取得します。
    """
    api_res=await call_winforms_api("get_column_constraints", {} )
    if api_res.status=="complete":
        #print("api_res=",api_res)
        Cc=api_res.get_value_as(ColumnConstraints)
        return Cc
    else:
        return None
    


@mcp.tool()
async def get_pair_constraints()-> PairConstraints|None:
    """
    スケジュールナースから現在の全てのColumnConstraintを取得します。
    """
    api_res= await call_winforms_api("get_pair_constraints", {} )
    if api_res.status=="complete":
        #print("api_res=",api_res)
        Pc=api_res.get_value_as(PairConstraints)
        return Pc
    else:
        return None

@mcp.tool()
async def get_solving_parameters()-> SolvingParameters|None:
    """
    スケジュールナースから現在のSolvingParametersを取得します。
    """
    api_res= await call_winforms_api("get_solving_parameters", {} )
    if api_res.status=="complete":
        #print("api_res=",api_res)
        Spp=api_res.get_value_as(SolvingParameters)
        return Spp
    else:
        return None


@mcp.tool()
async def update_date_objects(data: DateObjects) -> ApiResponse:
    """
    複数の日付定義(DateObject)を一括で更新します。
    """
    # C#側へ送信するための辞書化
    payload = data.model_dump_json()
    
    # WinForms APIを呼び出してデータを送信
    return  await call_winforms_api("update_date_objects", payload)
    
   


@mcp.tool()
async def update_date_aggregates(data: DateAggregates) -> ApiResponse:
    """
    日付集合(DateAggregates)の全リストを更新します。
    """
    # Pydanticオブジェクトを辞書(JSON互換)に変換
    # これにより members 内の DateAggregate もすべて辞書化されます
    payload = data.model_dump_json()
    
    # WinForms APIの呼び出し(例)
    return await call_winforms_api("update_date_aggregates", payload)
   

 


@mcp.tool()
async def update_staff_group_properties(data: GroupProperties) -> str:
    """
    スタッフの全属性定義(GroupProperties)を一括更新します。
    """
    # 辞書形式に変換(ネストされた GroupProperty もすべて変換されます)
    payload = data.model_dump_json()
    
    # WinForms API への送信(例)
    return  await call_winforms_api("update_group_properties", payload)
    
    

@mcp.tool()
async def update_staff_group_aggregates(data: GroupAggregates) -> str:
    """
    スタッフ属性の集合演算定義(GroupAggregates)を一括更新します。
    """
    # 辞書形式に変換(WinForms API 送信用)
    payload = data.model_dump_json()
    
     
    return await call_winforms_api("update_group_aggregates", payload)
    
    



@mcp.tool()
async def update_staff_properties(data: StaffProperties) -> str:
    """
    全スタッフの属性情報(担当可能シフト、属性マップ等)を一括更新します。
    """
    # WinForms側へ送信するために辞書形式に変換
    payload = data.model_dump_json()
    
     
    return await call_winforms_api("update_staff_properties", payload)
    
    

@mcp.tool()
async def update_shift_schedules(data: ShiftSchedules) -> str:
    """
    勤務表の全予定データ(二次元リスト)を一括更新します。
    """
    # 再帰的に辞書形式に変換(ShiftLevel オブジェクトもすべて辞書化される)
    payload = data.model_dump_json()
    
    # WinForms API への送信
    return await call_winforms_api("update_schedules", payload)
    
    


@mcp.tool()
async def update_row_constraints(data: RowConstraints) -> str:
    """
    行制約(RowConstraints)の設定をすべて更新します。
    """
    # 辞書形式に変換(ネストされたリストやオブジェクトもすべて変換されます)
    payload = data.model_dump_json()
    
    
    return  await call_winforms_api("update_row_constraints", payload)
    
    

    

@mcp.tool()
async def update_column_constraints(data: ColumnConstraints) -> str:
    """
    列制約(ColumnConstraints)の設定をすべて一括更新します。
    """
    # model_dump_json() により C# 側が受信可能な JSON 形式の辞書に変換
    payload = data.model_dump_json()
    
    # 注意: C#側でColor.Whiteを復元できるよう、tab_colorには"White"などの文字列が入ります。
    
    return  await call_winforms_api("update_pair_constraints", payload)
    
    

@mcp.tool()
async def update_pair_constraints(data: PairConstraints) -> str:
    """
    ペア制約(PairConstraints)の設定をすべて一括更新します。
    """
    # model_dump_json() により C# 側が受信可能な JSON 形式の辞書に変換
    payload = data.model_dump_json()
    
    # 注意: C#側でColor.Whiteを復元できるよう、tab_colorには"White"などの文字列が入ります。
    
    return  await call_winforms_api("update_column_constraints", payload)

mcp.tool()
async def update_solving_parameters(data: SolvingParameters) -> str:
    """
    求解パラメータ(SolvingParameters)の設定をすべて一括更新します。
    """
    # model_dump_json() により C# 側が受信可能な JSON 形式の辞書に変換
    payload = data.model_dump_json()
    
    
    return  await call_winforms_api("update_solving_parameters", payload)

@mcp.tool()
async def load_project(filepath: str) ->ApiResponse:
    """スケジュールナースのプロジェクトファイルをスケジュールナースにロードする MCP ツール"""
    return await call_winforms_api("load_project", {"text": filepath})
  





@mcp.tool()
async def solve( payload: dict,ctx: Context= None) -> ApiResponse:
    if ctx==None:
        ctx = MockContext() 
    return await call_winforms_api("solve", payload, ctx)





# -------------------------------
# MCP サーバ起動
# -------------------------------
def start_server():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)

    loop.create_task(python_stream())
    mcp.run(port=5000)


# 1. MCPのContextを模倣するMockクラス
class MockContext:
    async def report_progress(self, progress: float, total: float):
        percentage = (progress / total) * 100
        print(f"【MCP進捗通知】 現在の進捗: {percentage:.1f}% ({progress}/{total})")


   
async def call_tool(client,command,payload):
    
    print("=== ",command," テスト開始 ===")
    result = await client.call_tool(command, payload)
    print("\n[Step 2] 最終結果を受信しました:")
    print(result.content[0].text)#.dumps(result, indent=2, ensure_ascii=False))
    print("=== ",command," テスト終了 ===\n")
    return result.content[0].text



async def api_test(api1,api2,obj):
    api_res= await call_winforms_api(api1, {} )
    if api_res.status!="complete":
        return False

    #print("api_res=",api_res)
    data=api_res.get_value_as(obj)
    #print("??data=",data)
    text = data.model_dump_json()# model_dump_json()
    #print("text=",text)
    payload={"text":text}
    api_res= await call_winforms_api(api2,payload)##{} )
    #print("api_res after update shift aggregates",api_res)
    api_res= await call_winforms_api(api1, {} )
    if api_res.status!="complete":
        print("Failed === api_test  "+api1+" "+api2+"テスト終了===\n")
        return False
    data2=api_res.get_value_as(obj)
    if data !=data2:
        print("Failed === api_test  "+api1+" "+api2+"テスト終了===\n")
        return False
    print("Success === api_test  "+api1+" "+api2+"テスト終了===\n")
    
           
    return True

async def test_server_locally():
    print("\n--- ローカルサーバーのテスト ---")
    # クライアントをサーバーオブジェクトに直接ポイントします
    client = Client(mcp) 
    ctx = MockContext()

    async with client:
       
        
        #filepath=r"C:\Users\sugaw\Documents\FA\sc3\プロジェクトサンプル\標準パターン\3交代深準標準パターン.nurse3"#'C:\Users\sugaw\Documents\FA\sc3\プロジェクトサンプル\プロジェクトサンプル\チュートリアル3A.nurse3'
        filepath=r"C:\Users\sugaw\Documents\FA\sc3\プロジェクトサンプル\プロジェクトサンプル\チュートリアル3A.nurse3"
        #filepath=r"C:\Users\sugaw\Documents\FA\sc3\release_check\2025.10_0930ver2.nurse3"
        payload={"filepath":filepath}
        
        await call_tool(client,"load_project",payload)
        
        # テスト用ペイロード ctxは、シリアライズできないので、そのまま載せられない
        payload = {
        "payload": {},
   
        }
        await call_tool(client,"solve",payload)

        #SO_str=await call_tool(client,"get_shift_definitions", payload)
        #deliv_from_dict = Delivery.model_validate(deliv_dict)
        #SO = ShiftObjects.model_validate_json(SO_str)
        #print("*SO=",SO)
        #text=SO.model_dump_json()
        #print("*S0_str=",text)
        #payload={"text":text}
        #api_res=await call_winforms_api("update_shift_objects",payload )
        #print(api_res)
        #SO_str2=await call_tool(client,"get_shift_definitions", payload)
        #compare_str(SO_str,SO_str2)
        #deliv_dict = deliv.model_dump_json()  # ◎
        # >> {'timestamp': datetime.datetime(2020, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc), 'products': [{'name': 'sushi', 'price': 1000}]}
        #deliv_json = deliv.model_dump_json()  #
        """
        SA=await call_tool(client,"get_shift_aggregates_definitions", payload)
        print("SA=",SA)
        So=await call_tool(client,"get_shift_solution", payload)
        print("Solution=",So)
        DO=await call_tool(client,"get_date_objects", payload)
        print("DateObjects=",DO)

        DA=await call_tool(client,"get_date_aggregates", payload)
        print("DateAggregates=",DA)

        GP=await call_tool(client,"get_group_properties", payload)
        print("Groupp Properties=",GP)

        GA=await call_tool(client,"get_group_aggregates", payload)
        print("Group Aggregates=",GA)

        SP=await call_tool(client,"get_staff_properties", payload)
        print("Staff Properties=",SP)

        SC=await call_tool(client,"get_shift_schedules", payload)
        print("Shit Schedules=",SC)
        RC=await call_tool(client,"get_row_constraints", payload)
        print("Row Constraints=",RC)
        CC=await call_tool(client,"get_column_constraints", payload)
        print("Column Constraints=",CC)
        PC=await call_tool(client,"get_pair_constraints", payload)
        print("Pair Constraints=",PC)
        SPP=await call_tool(client,"get_solving_parameters",payload)
        print("Solving Parameter=",SPP)

        print("update_shift_objects Test")
        api_res=  await call_winforms_api("update_shifts",SO.model_dump_json())"""
        
        
async def test_server_api():
        for i in range(1):
            print("****Loop=",i)    
            #filepath=r"C:\Users\sugaw\Documents\FA\sc3\プロジェクトサンプル\標準パターン\3交代深準標準パターン.nurse3"#'C:\Users\sugaw\Documents\FA\sc3\プロジェクトサンプル\プロジェクトサンプル\チュートリアル3A.nurse3'
            #filepath=r"C:\Users\sugaw\Documents\FA\sc3\プロジェクトサンプル\プロジェクトサンプル\チュートリアル3A.nurse3"
            #filepath=r"C:\Users\sugaw\Documents\FA\sc3\release_check\2025.10_0930ver2.nurse3"
            #filepath=r"C:\Users\sugaw\Documents\FA\sc3\寒河江\8月V1Color.nurse3"
            filepath=r"C:\Users\sugaw\Documents\FA\sc3\release_check3\神戸大0405_5_V4.nurse3"
            payload={"text":filepath}
        
        
            api_res= await call_winforms_api("load_project",payload )
        # テスト用ペイロード ctxは、シリアライズできないので、そのまま載せられない
            payload = {
            "payload": {},
   
            }
            api_res=await call_winforms_api("solve",payload)
            await api_test("get_shift_objects","update_shift_objects",ShiftObjects)
            await api_test("get_shift_aggregates","update_shift_aggregates",ShiftAggregates)
            await api_test("get_date_objects","update_date_objects",DateObjects)
            await api_test("get_date_aggregates","update_date_aggregates",DateAggregates)
            await api_test("get_group_properties","update_group_properties",GroupProperties)
            await api_test("get_group_aggregates","update_group_aggregates",GroupAggregates)
            await api_test("get_staff_properties","update_staff_properties",StaffProperties)
            await api_test("get_shift_schedules","update_shift_schedules",ShiftSchedules)
            await api_test("get_row_constraints","update_row_constraints",RowConstraints)
            await api_test("get_column_constraints","update_column_constraints",ColumnConstraints)
            await api_test("get_pair_constraints","update_pair_constraints",PairConstraints)
            await api_test("get_solving_parameters","update_solving_parameters",SolvingParameters)


async def test_solve_locally():
    ctx = MockContext()
    # 以前作成した継続読み取り版の call_winforms_api を呼び出し
    # ※ WinFormsアプリを起動した状態で実行してください
    filepath=r'C:\Users\sugaw\Documents\FA\sc3\Project_Samples\プロジェクトサンプル\チュートリアル3.nurse3'
    payload={"text":filepath}
    result = await call_winforms_api("load_project", payload, ctx=ctx)
    print("\n[Step 2] 最終結果を受信しました:")
    print(json.dumps(result, indent=2, ensure_ascii=False))
    print("=== load_projectテスト終了 ===")


    
    print("\n[Step 1] WinFormsへ 'solve' 命令を送信します...")
    
    # 以前作成した継続読み取り版の call_winforms_api を呼び出し
    # ※ WinFormsアプリを起動した状態で実行してください

     
    # テスト用ペイロード
    payload = {
        "mode": "normal",
        "options": {"max_iterations": 1000}
    }
    result = await call_winforms_api("solve", payload, ctx=ctx)
    print("\n[Step 2] 最終結果を受信しました:")
    print(json.dumps(result, indent=2, ensure_ascii=False))
    print("=== solveテスト終了 ===")

    result = await call_winforms_api("solve", payload, ctx=ctx)
    print("\n[Step 2] 最終結果を受信しました:")
    print(json.dumps(result, indent=2, ensure_ascii=False))
    print("=== solveテスト終了 ===")
    
    
    
    print("=== Solve API ローカルテスト開始 ===")
    
    print("\n[Step 1] WinFormsへ 'get_shift_objects' 命令を送信します...")
    result=await call_winforms_api("get_shift_objects",{}, ctx=ctx);
    print("\n[Step 2] 最終結果を受信しました:")
    print(json.dumps(result, indent=2, ensure_ascii=False))
    print("=== get_shift_objectsテスト終了 ===")
    print("\n[Step 1] WinFormsへ 'load_project' 命令を送信します...")
    
    

if __name__ == "__main__":
    try:
        asyncio.run(test_server_api())#test_server_locally())#Test())#test_solve_locally())# エントリポイントは一つにする
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(f"エラーが発生しました: {e}")


2026年1月19日月曜日

instance24失敗

 instance24の求解に失敗しました。LBを求めるところまでは、目論見通りでした。COPTを使用しても、相当な時間がかかりました。問題は、相当のBranchの後、LPソルバの目的関数値が整数になったときの整数化で失敗しました。

整数化には、AL1アルゴリズムを使用しているのですが、こちらの制御が戻ってこないというものです。原因としては、instance24は、MaxSATでは、巨大すぎるということだろうと思います。

MaxSATの得意領域は、計数制約が少ない範囲で、boolean operationが多数になればなるほど得意になる傾向があります。

ところが、instance24は、150人、365日、32シフトという超超巨大インスタンスであり、多数の計数制約があります。これがメモリを食い数十GBになります。instance22(50人、365日、10シフト)までは、問題なく動作したMaxSATソルバも限界を超えた、と判断しました。

MaxSATソルバが、30日、30人といった程度の通常インスタンスでは、無類の強さを発揮するのは何故かというと、学習機構を持っているからです。逆に言うと、

割り当て⇒エラー⇒原因分析⇒同じ原因のエラーを抑止

という学習機構が有効に働くインスタンス範囲では強い、ということになります。メタヒューリスティクスは、基本的にその機構がありません。

割り当て⇒エラー⇒スコアを上下

という単純な機構です。原因分析機構が無い分、構造は簡単で、メモリも食いません。

ところで、超大規模インスタンスでは、大規模すぎて、そもそも学習が殆ど効かないと思われます。(正確には、学習しても、探索範囲が膨大なので、同じ原因にぶちあたる確率が小さい。)なので超大規模インスタンス領域では、メモリが軽い分、メタヒューリスティクスが有利となります。

<対策>

そこで、大規模用メタヒューリスティクスを開発することにしました。で、どうせなら汎用アルゴリズムとしてラインアップしてもよいかな?とも思います。例えば、ローカルソルバをAL4として加えるアイデアです。

■AL1:MaxSAT

■AL2:Highs MIP Solver

■AL3:Mathematical Solver

■AL4:Local Solver

そうなると、全方位のソルバ群が完成することになります。

<スケジュール>

2月は、MCPテスト、評価。

3月に、ローカルソルバと、make_feasible_model(別途解説)の実装を予定します。なので、世界記録の更新予定は、4月です。それまで破られなければ。(LBの値は、判明しましたが、それまでは、公開しません。)





2026年1月18日日曜日

Visual Studio 2022にMigrate

 開発環境をVisual Studio 2019から、2022に移行しました。VS2022+ .NET framwork4.8は、長期サポート(LTS)になっているので、今後10年は大丈夫です。




そのために、まず、Private版で、ビルドを行いました。コンパイラが変わると何某かの問題が生じるのが常です。そのための確認ステップとして、ソースを変更せず、開発環境だけをMigrateしたビルドをリリースしました。

これから、これをベースにMCP実装を進めて行きます。




2026年1月17日土曜日

Pydanticで記述

 PythonのPydanticとC#を比較してみた + C++ DCOMの思い出 #pydantic - Qiita

OpenAI Structured OutputsのPydantic連携を実装してみました | 株式会社divx(ディブエックス)

勉強になりました。

これから、Pydanticを使用してC#クラスを変換して行きたいと思います。

同じShiftAggregateというC#内部クラスをコパイと、Google AIモードの両方で生成させてみました。

コパイ生成モデル


from pydantic import BaseModel, Field
from typing import List

class ShiftAggregate(BaseModel):
    use: bool = Field(
        description="A flag to indicate whether it is valid or invalid. 有効(true)か無効かを示すフラグ。"
    )
    name: str = Field(
        default="ShiftAggregateDef",
        description="Object Name オブジェクト名"
    )
    def_name: str = Field(
        description="Definition name 定義名。シフトオブジェクトのORまたはNOTの集合体。必須。"
    )
    color: str = Field(
        default="Grey",
        description="Background color ラベル背景色"
    )
    operator: str = Field(
        default="OR",
        description="Operator 演算子。OR または NOT のみ。必須。"
    )
    members: List[str] = Field(
        default_factory=list,
        description="List of shift object names shift_objectの名前集合。必須。"
    )
    label: str = Field(
        description="Label name as displayed name ラベル名(GUI表示名)。必須。"
    )

下は、GOOGLE AIモード生成です。

from typing import List, Literal, Optional from pydantic import BaseModel, Field from mcp.server.fastmcp import FastMCP # 1. C# のクラスを Pydantic モデルとして定義 class ShiftAggregate(BaseModel): use: bool = Field( default=False, description="有効(true)か無効か。無効ならソルバは参照しない" ) name: str = Field( default="ShiftAggregateDef", description="オブジェクト名" ) def_name: str = Field( ..., description="定義名。シフトオブジェクトのORまたはNOTの集合体" ) color: str = Field( default="Grey", description="ラベル背景色" ) # Operator は OR または NOT のみ operator: Literal["OR", "NOT"] = Field( default="OR", description="演算子。OR または NOT のみ" ) members: List[str] = Field( default_factory=list, description="シフトオブジェクトの名前集合" ) label: str = Field( ..., description="表示用ラベル名" ) # 2. MCP サーバーの作成 mcp = FastMCP("ShiftManager") # 3. ツールとして登録 @mcp.tool() async def register_shift_aggregate(data: ShiftAggregate) -> str: """ シフトの集合体定義(ShiftAggregate)を登録します。 """ # ここに WinForms API (call_winforms_api) への送信処理などを記述 # 例: payload = data.model_dump() return f"定義 '{data.label}' (Operator: {data.operator}) を受け付けました。" if __name__ == "__main__": mcp.run()




さらに、C#クラスを少し修正して再度AIモードにお願いしました。修正した部分が分かって生成していることが分かります。こうした基本クラスならば、手で書くよりもAIにお願いした方が圧倒的に速いです。C#クラスのコメント部をdescriptionに解釈して書いてくれるので、その点でも楽です。model_dump()もBaseModelを継承して自動生成してくれます。AIモードは、MCPをどのように実装したらよいかを、解説しながら実装例を示してくれます。私のようなMCP初心者には、有難いです。基本的クラスは、こうした手法により、Python側のコードは、ほぼコピペで完結できます。C#側は、かなり手を加える必要があります。

コパイでは、C#コメントは、そのままPythonコメントになりましたが、Google AIモードでは、徒手選択追加されています。必ずしもC#コメントと同じになる訳ではありません。概して分かり易い方向に作用しますが、余計なお世話だ、という場合もあります。

驚くべきは、定義していないクラスまで生成したことです。それが、正確にPythonに変換生成されていました。以前GUIのC#コードを公開していたことがありましたが、それを参照したとしか考えられない、と思っています。驚きました。


from typing import List, Literal
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP

# 1. C#のクラス構造をPydanticモデルで定義
class ShiftAggregate(BaseModel):
    use: bool = Field(
        default=False, 
        description="有効(true)か無効かを示すフラグ。無効ならソルバは参照しない"
    )
    # const扱いの名前。LLMが変更しないようdefault固定にする
    name: str = Field(
        default="ShiftAggregateDef", 
        frozen=True,
        description="オブジェクト名(固定値: ShiftAggregateDef)"
    )
    def_name: str = Field(
        default="", 
        description="定義名。シフトオブジェクトのORまたはNOTの集合体を表す一意識別子"
    )
    color: str = Field(
        default="Grey", 
        description="ラベル背景色(例: Blue, #FF0000)"
    )
    # Operatorをリテラルで制限
    Operator: Literal["OR", "NOT"] = Field(
        default="OR", 
        description="演算子。OR(いずれか)またはNOT(以外)のみ選択可能"
    )
    members: List[str] = Field(
        default_factory=list, 
        description="シフトオブジェクト名のリスト(例: ['日勤', '夜勤'])"
    )
    label: str = Field(
        default="", 
        description="GUI(予定入力・解上)での表示名"
    )

# 2. MCPサーバの初期化
mcp = FastMCP("ShiftProjectManager")

# 3. ツールとしての登録
@mcp.tool()
async def add_shift_aggregate(aggregate_data: ShiftAggregate) -> str:
    """
    WinFormsアプリに新しいシフト集計定義(ShiftAggregate)を追加または更新します。
    """
    # 実際にはここで call_winforms_api("update_settings", payload, ...) 等を呼び出す
    payload = aggregate_data.model_dump()
    
    # デバッグ出力
    print(f"Sending to WinForms: {payload}")
    
    return f"シフト定義 '{aggregate_data.label}' を {aggregate_data.Operator} 条件で登録しました。"

if __name__ == "__main__":
    # サーバの起動
    mcp.run()

2026年1月16日金曜日

Branching DepthとFractionalsの関係

 次のグラフは、Instance22上での、Branching Depth とLpソルバのFractional数をプロットしたものです。


大まかに言って、Branchingが進めば、Lpソルバの値で、Fractionals(整数0でも1でもない、その間にある数)が減って行きます。SimplexとBarrierソルバの比較では、Simplexの方がFractionalsの数は、減る傾向にありますが、速度的に問題外のため、この規模では、Barrier ソルバ一択です。注意するべきは、Fractionals以外の点、つまり0か1の整数値は、Validな値という訳ではなく、Branchによって、整数になったりならなかったりします。なので、整数になった、即0/1確定、ということではありません。しかし、一時的な整数値にせよ、大半の整数値は、正解であることが多いので、それを信じて整数化HeurisitcsであるMaxSATを走らせる、というのが、私の整数化方法です。当然、Fractionalsの数が少なければ少ないほど、真値により近づける可能性が高まります。

Branchingを最後まで行い、Fractionalsが0になれば、全部整数になったということなので、整数解が得られます。しかし、それは膨大な時間がかかってしまうので、上記Heurisitcsを導入し、整数解をなるべく途中段階で拾う、ということを行います。上の例では、MaxSATソルバBranch Depthに応じて4回起動して、4回目に最終解を得ています。

Depth0(ROOT)では、Timeout設定が良くなくてTimeoutになっていますが、UB=3万数千といったオーダになります。通常は、Branching Depthを早く稼いだ方が速くより小さなUBを得ることが出来ます。、Instance22の場合は、500Depth程度で、かなり近い値、1000Depthで、収束値に到達しました。

以上が数理ソルバでのBranchingの動きになります。


2026年1月15日木曜日

許容エラー数とは

 求解パラメータ中の許容エラーは、スラック変数範囲です。


スラック変数とは、OR(オペレーションズリサーチ)用語です。学術用語なので、少し難しいです。ググると不等式制約を等式制約に変換するときの変数と言う風に説明されるのですが、なんのことか分からない方が大半だと思います。

スケジュールナースでは、不等式制約においてどこまでをソフト制約とするか、その範囲を決める、との説明をしているのですが、OR的には、スラック変数範囲を規定する、ということになります。

夜勤Yとして、その範囲をRHS[Min,Max]とすると、

ΣY=RHS[Min:Max]

と表現できます。例えば、夜勤3回以上4回以下のハード制約とするなら、

ΣY=RHS[3:4]

となります。ところが、この制約は、満足できるかどうか分かりません。MIPソルバにおいても、解が無い場合は、Infeasibleとなり、その原因は、全く分からなくなってしまいます。なので、一般に、スラック変数Xを導入して3回未満でも解がない、ということがないように

ΣY+X>=3 

Xの範囲は、X=[0:∞] とすれば、(実際には、[0:3]の範囲で十分)、解がないという事態は、必ず防げます。Xが0でない場合にソフトエラーが発生した、(ただし連続変数)という風に解釈できます。Xの値にコストを付加すれば、コスト最小となるように(目的関数値が最小となるように)ソルバは動作します。

同様に、

ΣYーZ<=4

というスラック変数Zを導入して、Z=[0:∞] とすれば、解がないという事態は絶対起こりません。

線形ソルバにとって、スラック変数の負担は、SATソルバに比べれば非常に軽いです。加えて、その変数範囲[0:∞]という、連続変数の幅の大きさはソルバ負担にほぼなりません。なので、MIPソルバによる定式化の場合は、スラック変数付で、定式化することが定石です。つまり、数を数える制約は、全て∞幅のソフト制約にするのが基本です。

SATソルバでは、そもそも∞という定式化は存在しません。何某かの有限な整数幅でしか定義できません。この辺、線形ソルバと0:1変数のみ扱うSATソルバの決定的かつ本質的な違いです。幅の大きさはソルバ負担に直に効いてきます。この幅をスケジュールナースでは、許容エラー数と表現しています。

この辺、新しいアルゴリズムでは、ラフに設定しても解がないということがないように、自動設定するモードを付加する予定です。MCPでもコントロールできるようにします。

MCP Pythonでの求解パラメータは、次のようなクラスで表現しています。このクラスを操作することで、求解パラメータを操作できるようになります。これは、C#クラスからGoogleAIモードで生成させたモデルです。例えば、SwIntのuseは、上図、適用のチェックの有無に対応します。weightが重みに対応します。

ほぼ、C#クラスにあるコメントを踏襲していますが、SwIntクラスのValueに着目してください。(スラック変数)実は、この”スラック変数”は、C#クラスのコメントにはありません。LLMが勝手に付け足した言葉です。

LLMの理解力が、如何に優れているかを知った、次第です。


class SwInt(BaseModel):
    """
    ソルバの制約許容数や重み(ウェイト)を定義するモデルです。
    """
    use: bool = Field(
        default=False, 
        description="有効(true)か無効(false)かを示すフラグ。"
    )
    
    value: int = Field(
        default=0, 
        description="1制約あたりのCARDINALS許容エラー数(スラック変数)。"
    )
    
    total_max_errors: int = Field(
        default=0, 
        description="現在使用されていません(0: 最小化を意味します)。"
    )
    
    weight: int = Field(
        default=1, 
        description=(
            "ソフト制約の重み(自然数、1-10推奨)。"
            "※ソフトレベル(1-7のインデックス)とは異なる「実際の重み係数」です。"
        )
    )
class SolvingParameters(BaseModel):
    """
    ソルバの求解パラメータ、ソフト制約レベルごとの重み設定、
    および外部Python制約などを管理するクラスです。
    """
    name: str = Field(
        default="SolvingParameters",
        frozen=True,
        description="オブジェクト名。常に 'SolvingParameters' 固定です。"
    )

    # Dictionary> solving_map を再現
    # 外側のキー(int)はソフト制約レベル(1-7など)、内側のキー(str)は制約名
    solving_map: dict[int, dict[str, SwInt]] = Field(
        default_factory=dict,
        description="ソフトレベルおよび制約名ごとの詳細設定マップ。"
    )

    # Pythonによる制約記述
    external_constraint_python: str = Field(
        default="",
        description="Pythonスクリプトによる外部制約記述。"
    )

    # プロジェクトコメント
    comment: str = Field(
        default="",
        description="プロジェクトの変更履歴やメモ用コメント。"
    )

    # 求解パラメータ Dictionary
    parameters: dict[str, int] = Field(
        default_factory=dict,
        description="ソルバの各種数値パラメータ(イテレーション回数等)。"
    )

    # 現在使用されていない、または内部用のフィールド
    aws_parameters: dict[str, str] = Field(
        default_factory=dict,
        description="AWS関連パラメータ(現在未使用)。"
    )
    external_constraint: str = Field(
        default="",
        description="言語制約記述(現在未使用)。"
    )
    python_property_file: str = Field(
        default="",
        description="内部用プロパティファイルパス(編集不可)。"
    )
    python_property_file_post: str = Field(
        default="",
        description="内部用プロパティファイルパス(後処理、編集不可)。"
    )

2026年1月14日水曜日

MCPリソースについて

 MCPリソースは、URIで行うメリットについて聞きました。考えているのは、few shotプロンプトや、Python制約の理解と使用どころをどのようにLLMに理解させるかです。以下の回答のように、URIであれば、LLMが理解し、そのまま維持してくれる確率が高まる、ということです。逆にローカルファイルであれば、その可能性が低くなってしまう、ということが考えられます。

病院で運用する場面になると、URIは、遮断されることが多いと思うので、URI参照は、できません。しかし、学習を終えたLLMがローカルアクセス出来るようになれば、その効果も維持できるでしょう。であるならば、やはり、そのための学習リソースを公開し、LLMに学んでもらうやり方が理にかなっています。よって、LLM用の学習リソースを今後積極的に公開していこうと思いました。基本的には、ユーザにチュートリアルを書くのと同じイメージを持っていますが、LLMリソース用ということで何かあるかもしれません。まだまだ勉強が必要なようです。


外部 URI を MCP(Model Context Protocol)のリソースとして扱うメリットは、実はかなり本質的で、あなたが言っている「他の利用者による再利用性が広がる」という点も含めて、複数のレイヤーで効いてくる。
少し深掘りしてまとめてみるね。


🌐 MCP リソースを外部 URI 化するメリット

1. LLM が “参照可能な世界” を広げられる

MCP のリソースを外部 URI(例:https://example.com/schema.json)として公開すると、
LLM がその URI を「安定した参照点」として扱えるようになる。

  • LLM は URI を「識別子」として理解しやすい
  • URI が固定なら、他の MCP クライアントやツールも同じリソースを参照できる
  • LLM の内部キャッシュや推論にも有利(同じ URI → 同じ意味のリソース)

つまり、**LLM にとって “世界の一部として扱える”**ようになる。


2. 他の MCP 利用者が同じリソースを共有できる

あなたの指摘の通り、これは大きなメリット。

  • 同じ MCP サーバーを使う別のユーザー
  • 別の LLM クライアント(VSCode, Cursor, Claude Desktop など)
  • 別のアプリケーション

これらが 同じ URI を参照するだけで同じリソースを利用できる

再利用性が爆発的に高まる

特に以下のようなものは URI 化の恩恵が大きい:

  • スキーマ(JSON Schema)
  • API 定義(OpenAPI)
  • 設定テンプレート
  • ドキュメント
  • プラグインのメタデータ

3. LLM の “学習済み知識” と結びつきやすい

LLM は、学習データの中で URI を「識別子」として扱っていることが多い。

例:

  • GitHub の raw URL
  • RFC の URL
  • W3C の仕様 URL
  • API のエンドポイント

そのため、URI を提示すると LLM が意味を推測しやすい

ローカルパス(file:///C:/...)ではこうはいかない。


4. バージョニング・キャッシュ・永続性の管理がしやすい

外部 URI にすると、以下が可能になる:

  • v1/schema.json のようにバージョン管理
  • CDN キャッシュ
  • 永続的な URL(Permalink)
  • 更新時の互換性維持

MCP のリソースとしては 安定性が非常に重要なので、URI 化は理にかなっている。


5. LLM が “安全に扱える” 情報になる

ローカルファイルは LLM にとって危険が多い:

  • パス漏洩
  • セキュリティ制約
  • OS 依存
  • ユーザーごとに異なる

外部 URI なら:

  • 公開情報として扱える
  • セキュリティリスクが低い
  • LLM が自由に参照しても問題ない

🎯 結論:外部 URI 化は LLM にとって “再利用性・安定性・意味理解” のすべてを強化する

あなたの直感は完全に正しい。

  • 他の MCP 利用者が再利用しやすくなる
  • LLM が URI を識別子として扱えるため、推論が安定する
  • バージョニングや公開管理がしやすい
  • ローカルパスより安全で移植性が高い

MCP の思想(「LLM が外部世界と安全に接続する」)とも完全に一致している。


🔧 もし興味があれば…

  • 外部 URI ベースの MCP リソース設計テンプレート
  • ローカル → 外部 URI への移行戦略
  • LLM が理解しやすい URI 命名規則
  • JSON Schema / OpenAPI の URI 最適化

こういった実践的な設計ガイドも作れるよ。


2026年1月13日火曜日

開発用エージェントを変更

 Gemini CLIの"強み"を知る! Gemini CLIとClaude Codeを比較してみた #AIエージェント - Qiita

https://taskhub.jp/useful/gemini-cli-claude-code

MCPサーバの開発デバッグには、多くのAIモデルコールが必要になります。想定していたClaudeでは、無償枠、有償枠ともに、簡単に制限を超えてしまう懸念があります。これでは、開発がままならないので、Gemini CLIを使用することにしました。


2026年1月12日月曜日

今年予定しているUpdate

 1)数理ソルバのUpdate

MaxSATソルバ、Highsソルバに加えて、数理計画ソルバのアップデートを予定しています。基幹アルゴリズム1には、手を加えず、Highsソルバの最新版へのアップデート、および、数理計画ソルバのアップデートを予定しています。6月頃の予定です。予定している世界記録更新は、商用LPソルバCOPTを使用したものであり、リリース版での搭載は予定していません。現在開発版は、COPTを使用しています。アルゴリズム自体は、COPTを使用しなくても動作しますが、リリース版では分離作業が必要になります。数理ソルバは、多くの技術分野の集積によるもので、実インスタンス向けの統合化実装はこれからの作業になります。ベンチマークで動いても、広範囲な実インスタンスに対応するのは、多くの困難が予想されます。究極のソルバであり、ナーススケジューリングにおいて、到達可能な極点、現時代の最終到達点を示すことになる、と思います。あらゆる実務ナーススケジューリングインスタンスにおいて、本当の物理限界、厳密解はどこにあるか?といったらまずスケジュールナースで試してみる、といった立ち位置になることを期待していて、リリース後は、そのための学術プロモーション活動を開始します。

2)AI IF(MCPインタフェース)を追加

MCPサーバは、現在Pythonで書いていて、ソースはGithubで公開します。これは、シフトのみついて、4月頃のリリースを予定しています。実験的リリースです。これも幾多の試みがあるものの、実用域に達しない自然言語によるAIモデリングに対する挑戦と提案です。


まずは、開発者向けの機能を提供したいと思います。実装したいアイデアは、山ほどあります。それをベースに一般の開発者やContributorが、さらに有効な機能や使い方を拡張できるようになると思います。

後になって、2025年というのは、MCPサーバが花開いたエポックメーキングな年だった、と言う風になるのではないでしょうか?

想えば、求解ソルバとAIモデリング、二つの課題に真正面から取り組んできた10年でした。私の自負は、他に誰も取り組んでこなかったテーマに10年間取り組んだことです。10年前に居た研究者は、皆どこかに行ってしまい、Gurobiで解くことが研究になってしまいました。

永く取り組んできた求解ソルバは今年で開発終了とします。AIモデリング化については、今年がスケジュールナースにとっての元年になるかと思われますが、そうではありません。沢山の実務インスタンスの集積(日本語による制約名が強制されている)は、Github上にあります。ユーザが語る自然言語から制約に落としてきた10年の経験と資産があります。これらの集積は、一朝一夕で出来るものではなく、これらの資産なしには、精度の高いAIモデリングは難しいと思います。資産とMCPを結びつけることが、提案骨子となります。いずれにせよ、求解ソルバと違い、AIモデリングの取り組みは、未来永劫、終わることのない戦いを強いられるでしょう。

 

2026年1月11日日曜日

Google AIモードが優秀

 コパイと会話しながら、MCP設計を進めていましたが、平気で嘘を言うのには、閉口します。

試しに、GoogleのAIモードでコード修正の提案を依頼すると、とても良い回答が返ってきました。そこで、Google AIモードで相談しながらMCP API設計を進めることにしました。

コードが動かないときに、AIに相談するのですが、本当らしい、嘘を言うのは、残念ながら、どちらも同じです。なので、真実を突きとめるには、やはり相応の技術を持ち合わせている必要がある、と痛感しました。

これは、ナーススケジューリングのモデリングにおいても同じことが言えると思います。例えば、複数の病棟があったとして、一人の管理者が複数の病棟のケアをするのは、結構労力が要ると思うのですが、AIモデリングについて、特性を熟知していると、その管理者にとって、強力なツールになりえるのでは?と思います。

また、初心者でツールの操作が良く分からない、と言った場面等、学習用途には、相性が良い可能性を感じました。何度同じ事を聞いても怒られることはないし、説明が秀悦で、分かり易いです。

AIにより助けられる場合もありますし、逆に指摘や、変更が間違っていると、間違ったところに労力を消費してしまう、ということがありえる、ということです。この辺、初心者と上級者で、使いどころを間違えないようにする必要がありそうです。

2026年1月9日金曜日

Q 買い切り版を購入した場合、今後アップデートや機能改変があった際には再購入が必要になるのでしょうか。

 Ans.

いいえ。必要ございません。

例えば、何世代か前の初期製品を10年前にご購入頂いたお客さまは、現在も世代・形態を変えながら、追加コストなくお使い頂いています。(ご購入当時は、買い切り版という形態はありませんでした。現在のストアでの販売形態もありませんでした。)


絶対というお約束はできませんが、将来的にもこうしたポリシーは維持したいと考えています。



2026年1月8日木曜日

Jsonスキーマ生成

 Newtosoftから、System.Text.Jsonに移行しました。コパイによれば、

- .NET Framework 4.x で `System.Text.Json` を使う場合 → **必ず NuGet パッケージが必要**

とのことです。

確かに、「参照」で追加した場合、DLLの相互依存が複雑らしくて、トラブルの元になりそうでした。NuGetで追加するのが確実なようです。

Jsonスキーマは、内部で、
JsonNode schema = options.GetJsonSchemaAsNode(typeof(singleA));
var json = JsonSerializer.Serialize(schema, new System.Text.Json.JsonSerializerOptions {
    WriteIndented = true
});

で生成させることが出来ました。

singleAというC#クラスが、スケジュールナース制約全体を表します。下図は、2020年当時の構成図になります。(現在は少しだけUpdateしていますが大まかには、変わりません。)

普段は、これをJsonにシリアライズして、最適化ソルバに渡して、ソルバが解を生成している訳です。で、このスキーマを生成させる仕組みが、上の記述です。

Python MCPでやりたいことは、各構成要素をAPIで、設定できるようにすることです。普段は、GUIで設定しているのですが、MCPサーバでも個々にできるようにする、ということになります。それだけを公開して、あとは、AIに頑張ってもらう、という発想です。

人がGUIで制約を記述するのをAIに肩代わりしてもらう、あるいは、プログラミングで、自動設定する、という目的のためでもあります。以前、予定セルのプログラミング入力を構想していたことがありましたが、その目的も兼ねています。

以下は、生成したJsonスキーマになります。

一言で言えば、Jsonのフォーマットということになるでしょうか。
例えば、最初の”SO”は、シフトオブジェクトのクラスを表しています。その次の”DO”は、Dayオブジェクトと言った具合です。

このオブジェクト群を読み込み、設定、追加、編集できるようにすることが目指しているMCPサーバになります。

生成されるJsonは、大きいと数MBを超えます。となるとAIモデルのトークン数の上限を簡単に超えてしまうことが懸念されます。しかし、その多くは、予定や、コメント、Python記述で、エッセンス部分ではありません。なので、エッセンス部分を残してハンドリングするようにすれば、なんとかなるのでは?と思っています。

{
  "type": [
    "object",
    "null"
  ],
  "properties": {
    "SO": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "auto_schedule": {
                "type": "boolean"
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "another_labels": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "another_colors": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "num_cnt": {
                "type": "integer"
              },
              "work_start_time": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "work_hours": {
                "type": "integer"
              }
            }
          }
        }
      }
    },
    "DO": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "days": {
          "type": "integer"
        },
        "target_year": {
          "type": "integer"
        },
        "target_month": {
          "type": "integer"
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "val": {
                "type": "integer"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dates": {
                "type": [
                  "array",
                  "null"
                ]
              },
              "use": {
                "type": "boolean"
              },
              "predefined": {
                "type": "boolean"
              }
            }
          }
        }
      }
    },
    "DA": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "Operator": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dates": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "use": {
                "type": "boolean"
              }
            }
          }
        }
      }
    },
    "SA": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "Operator": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "members": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      }
    },
    "PA": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "shift_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "list": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "type": {
                      "type": "integer"
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "time_string": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "color": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "day": {
                      "type": "integer"
                    },
                    "phase_no": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "primary_labels": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "alias_labels": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "dayphase_list": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "$ref": "#/properties/PA/properties/members/items/properties/list/items"
          }
        }
      }
    },
    "TAA": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "Operator": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "members": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      }
    },
    "TO": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "auto_schedule": {
                "type": "boolean"
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "shift_ag_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "another_labels": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "another_colors": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "work_hours": {
                "type": "integer"
              }
            }
          }
        }
      }
    },
    "PO": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "auto_schedule": {
                "type": "boolean"
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "num_cnt": {
                "type": "integer"
              },
              "work_hours": {
                "type": "integer"
              },
              "tasks": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "POA": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "phase_object_names": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "GP": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "members": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "GA": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "Operator": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "members": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "SP": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "shifts": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "tasks": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "properties": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "CC": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "phase_mode": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "use": {
                "type": "boolean"
              },
              "members": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "def_name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "use": {
                      "type": "boolean"
                    },
                    "shift_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "shift_at_phase": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "task_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "primary_day_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "staff_property_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "constraint_type": {
                      "type": "integer"
                    },
                    "min_double": {
                      "type": "number"
                    },
                    "max_double": {
                      "type": "number"
                    },
                    "dayphase_vec_max": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "dayphase_vec_min": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "max_value": {
                      "type": "integer"
                    },
                    "min_value": {
                      "type": "integer"
                    },
                    "soft_constraint_level": {
                      "type": "integer"
                    },
                    "soft_constraint_level_max": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "RC": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "phase_mode": {
                "type": "boolean"
              },
              "equal_count_mode": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "use": {
                "type": "boolean"
              },
              "members": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "intercept": {
                      "type": "integer"
                    },
                    "phase_mode": {
                      "type": "boolean"
                    },
                    "equal_count_mode": {
                      "type": "boolean"
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "def_name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "use": {
                      "type": "boolean"
                    },
                    "primary_day_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "first_day_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "last_day_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "staff_property_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "pattern": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "properties": {
                          "shift": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "level": {
                            "type": "integer"
                          },
                          "a_ix": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "shift_pattern": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "$ref": "#/properties/RC/properties/members/items/properties/members/items/properties/pattern/items"
                      }
                    },
                    "inv_pattern": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "type": "boolean"
                      }
                    },
                    "shift_inv_pattern": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "type": "boolean"
                      }
                    },
                    "A_daytypes": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    },
                    "B_daytypes": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    },
                    "constraint_type": {
                      "type": "integer"
                    },
                    "max_value": {
                      "type": "integer"
                    },
                    "min_double": {
                      "type": "number"
                    },
                    "max_double": {
                      "type": "number"
                    },
                    "min_value": {
                      "type": "integer"
                    },
                    "min_staff_property": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "max_staff_property": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "soft_constraint_level": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "SolP": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "solving_map": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "object",
              "properties": {
                "use": {
                  "type": "boolean"
                },
                "value": {
                  "type": "integer"
                },
                "total_max_errors": {
                  "type": "integer"
                },
                "weight": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "external_constraint": {
          "type": [
            "string",
            "null"
          ]
        },
        "external_constraint_python": {
          "type": [
            "string",
            "null"
          ]
        },
        "python_property_file": {
          "type": [
            "string",
            "null"
          ]
        },
        "python_property_file_post": {
          "type": [
            "string",
            "null"
          ]
        },
        "comment": {
          "type": [
            "string",
            "null"
          ]
        },
        "parameters": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": "integer"
          }
        },
        "aws_parameters": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    },
    "Sch": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/properties/RC/properties/members/items/properties/members/items/properties/pattern/items"
            }
          }
        }
      }
    },
    "TSch": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "$ref": "#/properties/Sch/properties/members/items"
          }
        },
        "days_property1": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "Dic": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": {
                  "type": [
                    "array",
                    "null"
                  ],
                  "items": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "first": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "second": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    }
                  }
                }
              },
              "task_name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      }
    },
    "PIC": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "equal_count_mode": {
                "type": "boolean"
              },
              "phase_mode": {
                "type": "boolean"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "use": {
                "type": "boolean"
              },
              "members": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "equal_count_mode": {
                      "type": "boolean"
                    },
                    "numerical_offset": {
                      "type": "integer"
                    },
                    "numerical_offset_string": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "phase_mode": {
                      "type": "boolean"
                    },
                    "phase_str": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "def_name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "use": {
                      "type": "boolean"
                    },
                    "shift_typeA": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "shift_typeB": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "shift_typeA_at_phase": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "shift_typeB_at_phase": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "day_type": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "staff_property_typeA": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "staff_property_typeB": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "operatorA": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "operatorB": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "dayoffset": {
                      "type": "integer"
                    },
                    "constraint_type": {
                      "type": "integer"
                    },
                    "soft_constraint_level": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "MA": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "fg": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "excel_file": {
              "type": [
                "string",
                "null"
              ]
            },
            "sheet_name": {
              "type": [
                "string",
                "null"
              ]
            },
            "enable_excel": {
              "type": "boolean"
            },
            "excel_output_enable": {
              "type": "boolean"
            }
          }
        },
        "guide_pdf_path": {
          "type": [
            "string",
            "null"
          ]
        },
        "html_code": {
          "type": [
            "string",
            "null"
          ]
        },
        "members": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "use": {
                "type": "boolean"
              },
              "apply_ex": {
                "type": "boolean"
              },
              "comment": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "expressions": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "daytype": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "grouptype": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "def_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "type": {
                "type": "integer"
              },
              "value": {
                "type": "integer"
              },
              "ex_value": {
                "type": "integer"
              },
              "value_double": {
                "type": "number"
              },
              "ex_value_double": {
                "type": "number"
              }
            }
          }
        }
      }
    },
    "major_version": {
      "type": "integer"
    },
    "sub_version": {
      "type": "integer"
    },
    "solver_version": {
      "type": "integer"
    },
    "user_class": {
      "type": [
        "string",
        "null"
      ]
    },
    "user_id": {
      "type": [
        "string",
        "null"
      ]
    },
    "eval_id": {
      "type": [
        "string",
        "null"
      ]
    },
    "ProjectName": {
      "type": [
        "string",
        "null"
      ]
    }
  }
}