Skip to content

Commit a94d7db

Browse files
JarbasAlclaude
andcommitted
feat: session-aware game-skill + OCP playback (stacked on IntentLayers)
Per-session playback state in OVOSCommonPlaybackSkill (state emits via message.reply so they route to the originating session) and game_skill adopting the context-gated layers. OCPCommonPlaybackSkill deprecation is handled separately in #423. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 31a8bc8 commit a94d7db

8 files changed

Lines changed: 271 additions & 57 deletions

File tree

.claude/settings.local.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(PYTHONPATH=/home/miro/AgentWorkspaces/OpenVoiceOS/core/ovos-core pip install padacioso -q)",
5+
"Bash(PYTHONPATH=/home/miro/AgentWorkspaces/OpenVoiceOS/core/ovos-core python3 -c \"from ovoscope import get_minicroft, End2EndTest; print\\('ok'\\)\")",
6+
"Bash(python3 -c \"import padacioso; print\\(padacioso.__file__\\)\")",
7+
"Bash(pip install:*)",
8+
"Bash(python3 -m pytest test/unittests/skills/test_base.py::TestOVOSSkill::test_ask_yesno test/unittests/skills/test_base.py::TestOVOSSkill::test_ask_selection -v -p no:ovoscope)",
9+
"Bash(python3 -m pytest test/unittests/test_ask_e2e.py -v -p no:ovoscope)",
10+
"Bash(git add:*)",
11+
"Bash(git commit -m ':*)",
12+
"Bash(git push:*)",
13+
"Bash(python3 -c \"from ovos_option_matcher_fuzzy import FuzzyOptionMatcherPlugin; import inspect; print\\(inspect.signature\\(FuzzyOptionMatcherPlugin.match_option\\)\\)\")",
14+
"Bash(python3 -c \"from ovos_plugin_manager.templates.option_matcher import OptionMatcherEngine; import inspect; print\\(inspect.signature\\(OptionMatcherEngine.match_option\\)\\)\")",
15+
"Bash(python3:*)",
16+
"Bash(git:*)",
17+
"Bash(gh pr:*)",
18+
"Bash(gh run:*)",
19+
"Bash(PYTHONPATH=/home/miro/AgentWorkspaces/OpenVoiceOS/core/ovos-core python3 -m pytest test/unittests/test_ask_e2e.py -v --timeout=30)",
20+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python -m pytest test/unittests/test_ask_e2e.py -v -p no:ovoscope --timeout=30)",
21+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/pip install:*)",
22+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python -m pytest test/unittests/test_ask_e2e.py -v --timeout=30)",
23+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python -m ensurepip -q)",
24+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python -m pip install pytest pytest-timeout -q)",
25+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python -m ensurepip)",
26+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python -m pip install -e \".[test]\" -q)",
27+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python -m pytest test/unittests/test_ask_e2e.py -v --timeout=60 -s)",
28+
"Bash(/home/miro/AgentWorkspaces/OpenVoiceOS/.venv/bin/python:*)"
29+
]
30+
}
31+
}

AGENTS.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# AGENTS.md — ovos-workshop
2+
3+
Base classes, decorators, and helpers for building OpenVoiceOS skills and applications. This is the framework every OVOS skill subclasses (`OVOSSkill`, `FallbackSkill`, `OVOSCommonPlaybackSkill`, etc.) plus the skill loader/launcher.
4+
5+
## Setup
6+
7+
```bash
8+
pip install -e .
9+
pip install -e .[test] # adds ovos-core, ovoscope, pytest, pytest-cov, ovos-translate-server-plugin
10+
```
11+
12+
## Test
13+
14+
```bash
15+
pytest test/unittests
16+
pytest test/end2end
17+
```
18+
19+
The end2end suite expects the bundled fixture skills under `test/end2end/session/` to be installed first (see `coverage.yml` for the exact list: `skill-ovos-hello-world`, `skill-ovos-fallback-unknown`, `skill-ovos-fallback-unknownv1`, `skill-converse_test`).
20+
21+
## Lint/Typecheck
22+
23+
Lint runs in CI via the gh-automations `lint.yml@dev` reusable workflow. No local lint/typecheck config is committed.
24+
25+
## Layout
26+
27+
- `ovos_workshop/skills/` — skill base classes: `ovos.py` (`OVOSSkill`, the canonical base), `fallback.py`, `common_play.py` (`OVOSCommonPlaybackSkill`), `game_skill.py`, `active.py`, `passive.py`, `auto_translatable.py` (`UniversalSkill`/`UniversalFallback`), `idle_display_skill.py`, `converse.py`, `intent_provider.py`, `api.py`, `layers.py`.
28+
- `ovos_workshop/decorators/``intent_handler`/context decorators (`__init__.py`), `killable.py`, `layers.py`, `ocp.py`, `fallback_handler.py`.
29+
- `ovos_workshop/skill_launcher.py` — loads skill plugins/dirs (`find_skill_plugins`, `get_skill_directories`), defines `SKILL_BASE_CLASSES`; provides the `ovos-skill-launcher` console script.
30+
- `ovos_workshop/app.py``OVOSAbstractApplication` (a skill that can run without the intent service).
31+
- `ovos_workshop/intents.py`, `resource_files.py`, `settings.py`, `filesystem.py`, `permissions.py`, `backwards_compat.py` — supporting subsystems.
32+
- `test/unittests`, `test/end2end` — test suites.
33+
34+
Console-script entry point (this repo): `ovos-skill-launcher`. Downstream skills register under the `opm.skills` entry-point group and subclass these base classes — this repo defines that contract.
35+
36+
## Conventions
37+
38+
- Branches: work on `dev`, stable on `master`. Never `main`.
39+
- Never edit `ovos_workshop/version.py` — gh-automations bumps semver from conventional-commit prefixes (`feat:` / `fix:` / `feat!:`).
40+
- New repos private by default.
41+
- Commit identity: JarbasAi <jarbasai@mailfence.com>.
42+
- Reference `OpenVoiceOS/gh-automations` reusable workflows at `@dev`.
43+
- No Neon / `neon-*` references.
44+
- No meta-commentary in docs/commits/code (no history, no dates).
45+
- CI is provided by OpenVoiceOS/gh-automations.
46+
47+
## Gotchas
48+
49+
- Test directory is `test/` (not `tests/`); CI `test_path` is `test/unittests`.
50+
- `coverage.yml` is hand-rolled (not the gh-automations `coverage.yml@dev` reusable workflow) — it pins Python 3.14 in the runner while `build-tests` and the package target 3.9; touch with care.
51+
- `backwards_compat.py` carries deprecated shims explicitly marked for removal; do not rely on it for new code.
52+
- Many `# TODO` markers in `intents.py` and `common_play.py` reflect upstream OCP limitations, not quick fixes.

TODO.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# TODO — ovos-workshop
2+
3+
## Open issues
4+
5+
- [ ] #380 Dependency Dashboard
6+
- [ ] #370 Incomplete OCP keyword deregistration functionality in OVOSCommonPlaybackSkill
7+
- [ ] #367 Improve CSV parsing in load_ocp_keyword_from_csv method
8+
- [ ] #331 Converse does not respect mute
9+
- [ ] #203 feat - verbosity level
10+
- [ ] #202 feat - "local_only" kwarg in intent decorators
11+
- [ ] #194 settings changed decorator
12+
- [ ] #191 missing tests for conversational intents
13+
- [ ] #175 OVOSSkill full CRUD for events
14+
- [ ] #56 Proposal: adding metadata to dialog files
15+
16+
## Gaps
17+
18+
- [ ] `coverage.yml` is a hand-rolled workflow rather than the gh-automations `coverage.yml@dev` reusable workflow; it pins runner Python 3.14 while the package supports 3.9.
19+
- [ ] Committed scratch artifact `downstream_report.txt` is tracked in the repo root.
20+
- [ ] `ovos_workshop.egg-info/` present in the working tree (not tracked; confirm `.gitignore` covers it).
21+
- [ ] Missing tests for conversational intents (tracked as #191).
22+
23+
## Code TODOs
24+
25+
- [ ] `ovos_workshop/intents.py:98` at least one should support aliases
26+
- [ ] `ovos_workshop/intents.py:119` CLIENT_ENTITY_NAME magic string unclear
27+
- [ ] `ovos_workshop/intents.py:404` consider properties with setters to prevent duplicates
28+
- [ ] `ovos_workshop/intents.py:521` will create duplicates of already detached intents
29+
- [ ] `ovos_workshop/backwards_compat.py:8` remove this file in next stable release
30+
- [ ] `ovos_workshop/skills/common_play.py:201` aliases per lang
31+
- [ ] `ovos_workshop/skills/common_play.py:358` not yet supported upstream
32+
- [ ] `ovos_workshop/skills/common_play.py:373` wont accept methods with killable_event
33+
- [ ] `ovos_workshop/skills/idle_display_skill.py:65` rm in ovos-gui, only for compat

ovos_workshop/skills/common_play.py

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from typing import List, Callable, Optional, Dict
1818

1919
from ovos_bus_client import Message
20+
from ovos_bus_client.message import dig_for_message
21+
from ovos_bus_client.session import SessionManager
2022
from ovos_config.locations import get_xdg_cache_save_path
2123
from ovos_utils import camel_case_split
2224
from ovos_utils.log import LOG
@@ -109,15 +111,44 @@ def __init__(self, *args,
109111
self.__prev_handler = prev_handler
110112
self.__resume_handler = resume_handler
111113
self._stop_event = Event()
112-
self._playing = Event()
113-
self._paused = Event()
114+
# OCP is session aware: track playing/paused state per session_id so
115+
# several sessions can play (and pause/stop) independently
116+
self._playing_sessions = set()
117+
self._paused_sessions = set()
114118
# TODO new default icon
115119
self.skill_icon = skill_icon or ""
116120

117121
self.ocp_matchers: Dict[str, AhocorasickNER] = {}
118122
self._ocp_ents: Dict[str, List[str]] = {}
119123
super().__init__(*args, **kwargs)
120124

125+
# --- per-session playback state (OCP is session aware) ------------------
126+
@staticmethod
127+
def get_session_id(message: Optional[Message] = None) -> str:
128+
"""Resolve the session id for the current/given message."""
129+
message = message or dig_for_message()
130+
return SessionManager.get(message).session_id
131+
132+
def is_playing_in(self, session_id: str) -> bool:
133+
return session_id in self._playing_sessions
134+
135+
def is_paused_in(self, session_id: str) -> bool:
136+
return session_id in self._paused_sessions
137+
138+
@property
139+
def playing_sessions(self) -> List[str]:
140+
return list(self._playing_sessions)
141+
142+
@property
143+
def is_playing(self) -> bool:
144+
"""True if the *current* session is playing (back-compat property)."""
145+
return self.is_playing_in(self.get_session_id())
146+
147+
@property
148+
def is_paused(self) -> bool:
149+
"""True if the *current* session is paused (back-compat property)."""
150+
return self.is_paused_in(self.get_session_id())
151+
121152
def _read_skill_name_voc(self):
122153
"""
123154
Load skill name aliases from a vocabulary file or generate them from the class name if not found.
@@ -456,14 +487,15 @@ def __handle_ocp_play(self, message):
456487
457488
If a playback handler is registered, it is called with the message if accepted, and the player state is set to PLAYING. Logs an error if no playback handler is implemented.
458489
"""
459-
self._playing.set()
460-
self._paused.clear()
490+
sid = self.get_session_id(message)
491+
self._playing_sessions.add(sid)
492+
self._paused_sessions.discard(sid)
461493
if self.__playback_handler:
462494
params = signature(self.__playback_handler).parameters
463495
kwargs = {"message": message} if "message" in params else {}
464496
self.__playback_handler(**kwargs)
465-
self.bus.emit(Message("ovos.common_play.player.state",
466-
{"state": PlayerState.PLAYING}))
497+
self.bus.emit(message.reply("ovos.common_play.player.state",
498+
{"state": PlayerState.PLAYING}))
467499
else:
468500
LOG.error(f"Playback requested but {self.skill_id} handler not "
469501
"implemented")
@@ -474,13 +506,14 @@ def __handle_ocp_pause(self, message):
474506
475507
If no pause handler is implemented, logs an error.
476508
"""
477-
self._paused.set()
509+
sid = self.get_session_id(message)
510+
self._paused_sessions.add(sid)
478511
if self.__pause_handler:
479512
params = signature(self.__pause_handler).parameters
480513
kwargs = {"message": message} if "message" in params else {}
481514
if self.__pause_handler(**kwargs):
482-
self.bus.emit(Message("ovos.common_play.player.state",
483-
{"state": PlayerState.PAUSED}))
515+
self.bus.emit(message.reply("ovos.common_play.player.state",
516+
{"state": PlayerState.PAUSED}))
484517
else:
485518
LOG.error(f"Pause requested but {self.skill_id} handler not "
486519
"implemented")
@@ -489,13 +522,14 @@ def __handle_ocp_resume(self, message):
489522
"""
490523
Handles OCP resume requests by invoking the registered resume handler and updating the player state to PLAYING if successful. Logs an error if no resume handler is implemented.
491524
"""
492-
self._paused.clear()
525+
sid = self.get_session_id(message)
526+
self._paused_sessions.discard(sid)
493527
if self.__resume_handler:
494528
params = signature(self.__resume_handler).parameters
495529
kwargs = {"message": message} if "message" in params else {}
496530
if self.__resume_handler(**kwargs):
497-
self.bus.emit(Message("ovos.common_play.player.state",
498-
{"state": PlayerState.PLAYING}))
531+
self.bus.emit(message.reply("ovos.common_play.player.state",
532+
{"state": PlayerState.PLAYING}))
499533
else:
500534
LOG.error(f"Resume requested but {self.skill_id} handler not "
501535
"implemented")
@@ -520,13 +554,14 @@ def __handle_ocp_prev(self, message):
520554

521555
def __handle_ocp_stop(self, message):
522556
# for skills managing their own playback
523-
if self._playing.is_set():
524-
self._paused.clear()
557+
sid = self.get_session_id(message)
558+
if sid in self._playing_sessions:
559+
self._paused_sessions.discard(sid)
525560
self.stop()
526561
self.gui.release()
527-
self.bus.emit(Message("ovos.common_play.player.state",
528-
{"state": PlayerState.STOPPED}))
529-
self._playing.clear()
562+
self.bus.emit(message.reply("ovos.common_play.player.state",
563+
{"state": PlayerState.STOPPED}))
564+
self._playing_sessions.discard(sid)
530565

531566
def __handle_stop_search(self, message):
532567
self._stop_event.set()

0 commit comments

Comments
 (0)