esimu-core 0.2.0b5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. esimu_core-0.2.0b5/LICENSE +22 -0
  2. esimu_core-0.2.0b5/PKG-INFO +58 -0
  3. esimu_core-0.2.0b5/README.md +25 -0
  4. esimu_core-0.2.0b5/esimu_core/__init__.py +17 -0
  5. esimu_core-0.2.0b5/esimu_core/ai/__init__.py +31 -0
  6. esimu_core-0.2.0b5/esimu_core/ai/config.py +114 -0
  7. esimu_core-0.2.0b5/esimu_core/ai/parsing.py +45 -0
  8. esimu_core-0.2.0b5/esimu_core/ai/policy.py +80 -0
  9. esimu_core-0.2.0b5/esimu_core/ai/service.py +422 -0
  10. esimu_core-0.2.0b5/esimu_core/ai/transport.py +151 -0
  11. esimu_core-0.2.0b5/esimu_core/cli.py +106 -0
  12. esimu_core-0.2.0b5/esimu_core/content/__init__.py +424 -0
  13. esimu_core-0.2.0b5/esimu_core/domain/__init__.py +5 -0
  14. esimu_core-0.2.0b5/esimu_core/domain/actions.py +85 -0
  15. esimu_core-0.2.0b5/esimu_core/domain/effects.py +200 -0
  16. esimu_core-0.2.0b5/esimu_core/domain/semester.py +259 -0
  17. esimu_core-0.2.0b5/esimu_core/lifecycle/__init__.py +238 -0
  18. esimu_core-0.2.0b5/esimu_core/py.typed +1 -0
  19. esimu_core-0.2.0b5/esimu_core/runtime/__init__.py +2 -0
  20. esimu_core-0.2.0b5/esimu_core/runtime/actions.py +37 -0
  21. esimu_core-0.2.0b5/esimu_core/runtime/clock.py +51 -0
  22. esimu_core-0.2.0b5/esimu_core/runtime/cooldowns.py +43 -0
  23. esimu_core-0.2.0b5/esimu_core/runtime/snapshot.py +179 -0
  24. esimu_core-0.2.0b5/esimu_core/runtime/state.py +90 -0
  25. esimu_core-0.2.0b5/esimu_core/runtime/tasks.py +62 -0
  26. esimu_core-0.2.0b5/esimu_core/scaffold/esimu-starter.zip +0 -0
  27. esimu_core-0.2.0b5/esimu_core/scaffold.py +518 -0
  28. esimu_core-0.2.0b5/esimu_core/world/__init__.py +2 -0
  29. esimu_core-0.2.0b5/esimu_core/world/balance.py +233 -0
  30. esimu_core-0.2.0b5/esimu_core/world/catalog.py +206 -0
  31. esimu_core-0.2.0b5/esimu_core/world/items.py +350 -0
  32. esimu_core-0.2.0b5/esimu_core/world/prompts.py +74 -0
  33. esimu_core-0.2.0b5/esimu_core/world/stat_definitions.py +302 -0
  34. esimu_core-0.2.0b5/esimu_core/world/story.py +148 -0
  35. esimu_core-0.2.0b5/esimu_core/world/theme.py +161 -0
  36. esimu_core-0.2.0b5/esimu_core/world/theme_contract.py +571 -0
  37. esimu_core-0.2.0b5/esimu_core/world/theme_paths.py +125 -0
  38. esimu_core-0.2.0b5/esimu_core/world/validation.py +174 -0
  39. esimu_core-0.2.0b5/esimu_core.egg-info/PKG-INFO +58 -0
  40. esimu_core-0.2.0b5/esimu_core.egg-info/SOURCES.txt +64 -0
  41. esimu_core-0.2.0b5/esimu_core.egg-info/dependency_links.txt +1 -0
  42. esimu_core-0.2.0b5/esimu_core.egg-info/entry_points.txt +3 -0
  43. esimu_core-0.2.0b5/esimu_core.egg-info/requires.txt +12 -0
  44. esimu_core-0.2.0b5/esimu_core.egg-info/top_level.txt +1 -0
  45. esimu_core-0.2.0b5/pyproject.toml +66 -0
  46. esimu_core-0.2.0b5/setup.cfg +4 -0
  47. esimu_core-0.2.0b5/tests/test_ai_core.py +264 -0
  48. esimu_core-0.2.0b5/tests/test_cli.py +36 -0
  49. esimu_core-0.2.0b5/tests/test_content_contracts.py +121 -0
  50. esimu_core-0.2.0b5/tests/test_demo_theme_smoke.py +150 -0
  51. esimu_core-0.2.0b5/tests/test_domain_actions.py +44 -0
  52. esimu_core-0.2.0b5/tests/test_domain_effects.py +115 -0
  53. esimu_core-0.2.0b5/tests/test_domain_semester.py +108 -0
  54. esimu_core-0.2.0b5/tests/test_lifecycle_contracts.py +107 -0
  55. esimu_core-0.2.0b5/tests/test_package_metadata.py +25 -0
  56. esimu_core-0.2.0b5/tests/test_project_bootstrap.py +144 -0
  57. esimu_core-0.2.0b5/tests/test_runtime_actions.py +31 -0
  58. esimu_core-0.2.0b5/tests/test_runtime_clock.py +22 -0
  59. esimu_core-0.2.0b5/tests/test_runtime_cooldowns.py +25 -0
  60. esimu_core-0.2.0b5/tests/test_runtime_snapshot.py +90 -0
  61. esimu_core-0.2.0b5/tests/test_runtime_state.py +37 -0
  62. esimu_core-0.2.0b5/tests/test_runtime_tasks.py +52 -0
  63. esimu_core-0.2.0b5/tests/test_theme_contract.py +147 -0
  64. esimu_core-0.2.0b5/tests/test_theme_prompts.py +26 -0
  65. esimu_core-0.2.0b5/tests/test_world_catalog.py +21 -0
  66. esimu_core-0.2.0b5/tests/test_world_validation.py +16 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pirate-608
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: esimu-core
3
+ Version: 0.2.0b5
4
+ Summary: Theme-driven core and project tooling for the esimu simulator framework.
5
+ Author: pirate-608
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://esimu.67656.fun/
8
+ Project-URL: Documentation, https://esimu.67656.fun/
9
+ Project-URL: Repository, https://github.com/pirate-608/esimu
10
+ Project-URL: Issues, https://github.com/pirate-608/esimu/issues
11
+ Keywords: simulation,game,framework,theme-pack
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pydantic>=2
23
+ Provides-Extra: ai
24
+ Requires-Dist: openai>=1.12; extra == "ai"
25
+ Provides-Extra: dev
26
+ Requires-Dist: aiosqlite>=0.20; extra == "dev"
27
+ Requires-Dist: fastapi>=0.115; extra == "dev"
28
+ Requires-Dist: httpx>=0.27; extra == "dev"
29
+ Requires-Dist: pytest>=8; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
31
+ Requires-Dist: ruff==0.16.2; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # esimu-core
35
+
36
+ `esimu-core` is the typed, I/O-independent core and project CLI for esimu.
37
+
38
+ ```powershell
39
+ python -m pip install "esimu-core[ai]==0.2.0b5"
40
+ esimu version
41
+ esimu new D:\projects\my-simulator --theme-id my-simulator
42
+ esimu validate --root D:\projects\my-simulator --theme my-simulator
43
+ ```
44
+
45
+ The base dependency is Pydantic. The optional `ai` extra adds the OpenAI SDK
46
+ transport. FastAPI, SQLite, WebSocket, and Vue live in the generated Starter,
47
+ not in the core package.
48
+
49
+ Core owns:
50
+
51
+ - theme and world-data contracts;
52
+ - stat, item, effect, semester, and lifecycle rules;
53
+ - runtime timing, state, payload, cooldown, and task helpers;
54
+ - neutral event, forum, and messenger content contracts;
55
+ - optional AI configuration, parsing, generation, and fallback policy;
56
+ - the self-contained `esimu new` scaffold bundle.
57
+
58
+ See `https://esimu.67656.fun/` for the authoring and compatibility guides.
@@ -0,0 +1,25 @@
1
+ # esimu-core
2
+
3
+ `esimu-core` is the typed, I/O-independent core and project CLI for esimu.
4
+
5
+ ```powershell
6
+ python -m pip install "esimu-core[ai]==0.2.0b5"
7
+ esimu version
8
+ esimu new D:\projects\my-simulator --theme-id my-simulator
9
+ esimu validate --root D:\projects\my-simulator --theme my-simulator
10
+ ```
11
+
12
+ The base dependency is Pydantic. The optional `ai` extra adds the OpenAI SDK
13
+ transport. FastAPI, SQLite, WebSocket, and Vue live in the generated Starter,
14
+ not in the core package.
15
+
16
+ Core owns:
17
+
18
+ - theme and world-data contracts;
19
+ - stat, item, effect, semester, and lifecycle rules;
20
+ - runtime timing, state, payload, cooldown, and task helpers;
21
+ - neutral event, forum, and messenger content contracts;
22
+ - optional AI configuration, parsing, generation, and fallback policy;
23
+ - the self-contained `esimu new` scaffold bundle.
24
+
25
+ See `https://esimu.67656.fun/` for the authoring and compatibility guides.
@@ -0,0 +1,17 @@
1
+ """Simulator framework backend core package.
2
+
3
+ Copyright (c) 2026 pirate-608. Licensed under the MIT License.
4
+ """
5
+
6
+ __version__ = "0.2.0b5"
7
+
8
+ THEME_SCHEMA_VERSION = 1
9
+ STATE_VERSION = 1
10
+ PROTOCOL_VERSION = 1
11
+
12
+ __all__ = [
13
+ "PROTOCOL_VERSION",
14
+ "STATE_VERSION",
15
+ "THEME_SCHEMA_VERSION",
16
+ "__version__",
17
+ ]
@@ -0,0 +1,31 @@
1
+ """Optional AI integration for esimu simulator adapters.
2
+
3
+ The package is safe to import without the OpenAI SDK. Constructing
4
+ ``OpenAICompatibleTransport`` requires the ``esimu-core[ai]`` extra.
5
+ """
6
+
7
+ from esimu_core.ai.config import (
8
+ AIModelConfig,
9
+ generic_model_config_from_env,
10
+ roleplay_model_config_from_env,
11
+ )
12
+ from esimu_core.ai.policy import ContentMode, ResolvedContent, resolve_content
13
+ from esimu_core.ai.service import AIContentService
14
+ from esimu_core.ai.transport import (
15
+ ChatTransport,
16
+ OpenAICompatibleTransport,
17
+ OpenAITransportRegistry,
18
+ )
19
+
20
+ __all__ = [
21
+ "AIContentService",
22
+ "AIModelConfig",
23
+ "ChatTransport",
24
+ "ContentMode",
25
+ "OpenAICompatibleTransport",
26
+ "OpenAITransportRegistry",
27
+ "ResolvedContent",
28
+ "generic_model_config_from_env",
29
+ "resolve_content",
30
+ "roleplay_model_config_from_env",
31
+ ]
@@ -0,0 +1,114 @@
1
+ """Model configuration for the optional esimu AI integration.
2
+
3
+ Copyright (c) 2026 pirate-608. Licensed under the MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from typing import Literal, Mapping
10
+
11
+ from pydantic import BaseModel, Field, field_validator, model_validator
12
+
13
+ PROVIDER_BASE_URLS: dict[str, str] = {
14
+ "openai": "https://api.openai.com/v1",
15
+ "deepseek": "https://api.deepseek.com",
16
+ "qwen": "https://dashscope.aliyuncs.com/compatible-mode/v1",
17
+ "glm": "https://open.bigmodel.cn/api/paas/v4",
18
+ "moonshot": "https://api.moonshot.cn/v1",
19
+ "minimax": "https://api.minimaxi.com/v1",
20
+ "ollama": "http://127.0.0.1:11434/v1",
21
+ }
22
+
23
+ RoleProfile = Literal["generic", "minimax_m2her"]
24
+
25
+
26
+ class AIModelConfig(BaseModel):
27
+ """One OpenAI-compatible model endpoint without application persistence."""
28
+
29
+ provider: str = "openai"
30
+ model: str = "gpt-4o-mini"
31
+ api_key: str | None = Field(default=None, repr=False)
32
+ base_url: str | None = None
33
+ timeout_seconds: float = Field(default=20.0, gt=0, le=300)
34
+ role_profile: RoleProfile = "generic"
35
+
36
+ @field_validator("provider", "model")
37
+ @classmethod
38
+ def strip_required_text(cls, value: str) -> str:
39
+ """Normalize required model identifiers."""
40
+ normalized = value.strip()
41
+ if not normalized:
42
+ raise ValueError("model provider/name cannot be empty")
43
+ return normalized
44
+
45
+ @field_validator("base_url")
46
+ @classmethod
47
+ def normalize_base_url(cls, value: str | None) -> str | None:
48
+ """Accept both SDK base URLs and older full completion endpoints."""
49
+ if value is None:
50
+ return None
51
+ normalized = value.strip().rstrip("/")
52
+ for suffix in ("/chat/completions", "/text/chatcompletion_v2"):
53
+ if normalized.endswith(suffix):
54
+ normalized = normalized[: -len(suffix)].rstrip("/")
55
+ return normalized or None
56
+
57
+ @model_validator(mode="after")
58
+ def fill_provider_url(self) -> "AIModelConfig":
59
+ """Use a known provider URL when the caller omitted an override."""
60
+ if self.base_url is None:
61
+ self.base_url = PROVIDER_BASE_URLS.get(self.provider.lower())
62
+ return self
63
+
64
+ @property
65
+ def is_configured(self) -> bool:
66
+ """Return whether the endpoint has enough information to be attempted."""
67
+ return bool(self.api_key or self.provider.lower() == "ollama")
68
+
69
+ @classmethod
70
+ def from_env(
71
+ cls,
72
+ prefix: str = "ESIMU_LLM",
73
+ *,
74
+ environ: Mapping[str, str] | None = None,
75
+ default_provider: str = "openai",
76
+ default_model: str = "gpt-4o-mini",
77
+ role_profile: RoleProfile = "generic",
78
+ ) -> "AIModelConfig":
79
+ """Load a model endpoint from a namespaced environment mapping."""
80
+ values = os.environ if environ is None else environ
81
+ provider = values.get(f"{prefix}_PROVIDER", default_provider)
82
+ timeout_raw = values.get(f"{prefix}_TIMEOUT_SECONDS", "20")
83
+ try:
84
+ timeout = float(timeout_raw)
85
+ except ValueError:
86
+ timeout = 20.0
87
+ return cls(
88
+ provider=provider,
89
+ model=values.get(f"{prefix}_MODEL", default_model),
90
+ api_key=values.get(f"{prefix}_API_KEY") or None,
91
+ base_url=values.get(f"{prefix}_BASE_URL") or None,
92
+ timeout_seconds=timeout,
93
+ role_profile=role_profile,
94
+ )
95
+
96
+
97
+ def generic_model_config_from_env(
98
+ environ: Mapping[str, str] | None = None,
99
+ ) -> AIModelConfig:
100
+ """Load the framework's general content model configuration."""
101
+ return AIModelConfig.from_env("ESIMU_LLM", environ=environ)
102
+
103
+
104
+ def roleplay_model_config_from_env(
105
+ environ: Mapping[str, str] | None = None,
106
+ ) -> AIModelConfig:
107
+ """Load the optional MiniMax M2-her role-play model configuration."""
108
+ return AIModelConfig.from_env(
109
+ "ESIMU_RP",
110
+ environ=environ,
111
+ default_provider="minimax",
112
+ default_model="M2-her",
113
+ role_profile="minimax_m2her",
114
+ )
@@ -0,0 +1,45 @@
1
+ """Defensive parsing helpers for model-generated content."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+
9
+ def json_object_from_text(content: object) -> dict[str, Any]:
10
+ """Extract one JSON object from plain text or a fenced response."""
11
+ if not isinstance(content, str) or not content.strip():
12
+ return {}
13
+ text = content.strip()
14
+ if text.startswith("```"):
15
+ lines = text.splitlines()
16
+ if lines:
17
+ lines = lines[1:]
18
+ if lines and lines[-1].strip().startswith("```"):
19
+ lines = lines[:-1]
20
+ text = "\n".join(lines).strip()
21
+ start = text.find("{")
22
+ end = text.rfind("}")
23
+ if start >= 0 and end >= start:
24
+ text = text[start : end + 1]
25
+ try:
26
+ value = json.loads(text)
27
+ except (json.JSONDecodeError, TypeError):
28
+ return {}
29
+ return value if isinstance(value, dict) else {}
30
+
31
+
32
+ def string_list(value: object, *, limit: int | None = None) -> list[str]:
33
+ """Return non-empty strings from an untrusted model value."""
34
+ if not isinstance(value, list):
35
+ return []
36
+ result = [item.strip() for item in value if isinstance(item, str) and item.strip()]
37
+ return result[:limit] if limit is not None else result
38
+
39
+
40
+ def dict_list(value: object, *, limit: int | None = None) -> list[dict[str, Any]]:
41
+ """Return dictionary entries from an untrusted model value."""
42
+ if not isinstance(value, list):
43
+ return []
44
+ result = [item for item in value if isinstance(item, dict)]
45
+ return result[:limit] if limit is not None else result
@@ -0,0 +1,80 @@
1
+ """Content-mode fallback policy shared by simulator adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import random
7
+ from dataclasses import dataclass
8
+ from typing import Awaitable, Callable, Generic, Literal, TypeVar
9
+
10
+ ContentMode = Literal["library", "hybrid", "ai"]
11
+ ContentSource = Literal["library", "ai", "fallback"]
12
+ T = TypeVar("T")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ResolvedContent(Generic[T]):
17
+ """One resolved content value plus degradation metadata."""
18
+
19
+ value: T | None
20
+ source: ContentSource
21
+ degraded: bool = False
22
+ error: str | None = None
23
+
24
+
25
+ async def _call(factory: Callable[[], T | Awaitable[T] | None]) -> T | None:
26
+ value = factory()
27
+ if inspect.isawaitable(value):
28
+ return await value
29
+ return value
30
+
31
+
32
+ async def resolve_content(
33
+ mode: ContentMode,
34
+ *,
35
+ ai_factory: Callable[[], T | Awaitable[T] | None] | None,
36
+ library_factory: Callable[[], T | Awaitable[T] | None],
37
+ hybrid_ai_probability: float = 0.35,
38
+ random_value: Callable[[], float] = random.random,
39
+ ) -> ResolvedContent[T]:
40
+ """Resolve AI/library content with deterministic degradation semantics.
41
+
42
+ Library mode never calls a model. AI mode tries the model first and falls
43
+ back locally. Hybrid mode chooses AI with the configured probability and
44
+ otherwise prefers the library, still using the other source if needed.
45
+ """
46
+ if mode not in {"library", "hybrid", "ai"}:
47
+ raise ValueError(f"unsupported content mode: {mode}")
48
+ probability = max(0.0, min(1.0, hybrid_ai_probability))
49
+ ai_first = mode == "ai" or (
50
+ mode == "hybrid" and ai_factory is not None and random_value() < probability
51
+ )
52
+ if mode == "library" or ai_factory is None:
53
+ return ResolvedContent(await _call(library_factory), "library")
54
+
55
+ first_name: ContentSource = "ai" if ai_first else "library"
56
+ second_name: ContentSource = "library" if ai_first else "ai"
57
+ first = ai_factory if ai_first else library_factory
58
+ second = library_factory if ai_first else ai_factory
59
+ first_error: str | None = None
60
+ try:
61
+ value = await _call(first)
62
+ if value is not None:
63
+ return ResolvedContent(value, first_name)
64
+ except Exception as exc: # adapters decide how to log/display degradation
65
+ first_error = str(exc)
66
+ try:
67
+ value = await _call(second)
68
+ return ResolvedContent(
69
+ value,
70
+ second_name if value is not None else "fallback",
71
+ degraded=first_name == "ai",
72
+ error=first_error,
73
+ )
74
+ except Exception as exc:
75
+ return ResolvedContent(
76
+ None,
77
+ "fallback",
78
+ degraded=first_name == "ai",
79
+ error=first_error or str(exc),
80
+ )