forked from balisujohn/localwriter
-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathservice.py
More file actions
165 lines (123 loc) · 5.98 KB
/
Copy pathservice.py
File metadata and controls
165 lines (123 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2024 John Balis
# Copyright (c) 2026 KeithCu (modifications and relicensing)
# Copyright (c) 2025-2026 quazardous (config, registries, build system)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Service infrastructure: base class and registry."""
from __future__ import annotations
from abc import ABC
from dataclasses import dataclass
from typing import Any, Generic, List, Protocol, TypeVar, cast
# ── FSM State Markers ─────────────────────────────────────────────
@dataclass(frozen=True)
class BaseState:
"""Marker base for immutable FSM state. Subclasses add domain fields."""
class Effect(Protocol):
"""Structural marker for side-effect descriptions (interpreted outside FSM)."""
StateT = TypeVar("StateT", bound=BaseState)
@dataclass(frozen=True)
class FsmTransition(Generic[StateT]):
"""Result of a pure transition: successor state and effects to run."""
state: StateT
effects: List[Any]
# ── Service Infrastructure ─────────────────────────────────────────
class ServiceBase(ABC):
"""Abstract base for services registered in the ServiceRegistry.
Services provide horizontal capabilities (document manipulation,
config access, LLM streaming, etc.) that modules and tools consume.
Attributes:
name: Unique service identifier (e.g. "document", "config").
"""
name: str | None = None
def initialize(self, ctx):
"""Called once during bootstrap with the UNO component context.
Override to perform setup that requires UNO (desktop access,
service manager, etc.).
Args:
ctx: UNO component context (com.sun.star.uno.XComponentContext).
"""
def shutdown(self):
"""Called on extension unload. Override to clean up."""
class ServiceRegistry:
"""Registry that holds all services and provides attribute access.
Usage::
services = ServiceRegistry()
services.register(my_document_service)
services.register(my_config_service)
# Access by name:
services.document.build_heading_tree(doc)
services.config.get("mcp.port")
# Or explicit:
services.get("document")
"""
def __init__(self):
self._services = {}
def register(self, name, instance):
"""Register an arbitrary object as a named service."""
if name in self._services:
raise ValueError(f"Service already registered: {name}")
self._services[name] = instance
def auto_discover(self, module):
"""Automatically discover and register ServiceBase subclasses in a module."""
import inspect
import logging
log = logging.getLogger("writeragent.services")
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, ServiceBase) and obj is not ServiceBase and obj.__module__ == module.__name__ and not inspect.isabstract(obj) and getattr(obj, "name", None):
try:
# Instantiate by passing the registry itself as 'services'
# We check if __init__ is overridden; if so, we pass self (the registry).
# Otherwise we call the default constructor.
if obj.__init__ is not object.__init__:
svc_instance = cast("Any", obj)(self)
else:
svc_instance = obj()
self.register(obj.name, svc_instance)
except (TypeError, ValueError, ImportError) as e:
log.error("Failed to instantiate service %s (TypeError/ValueError/ImportError): %s", obj.__name__, e)
except Exception as e:
log.error("Failed to instantiate service %s (unexpected): %s", obj.__name__, e)
def get(self, name):
"""Get a service by name, or None if not registered."""
return self._services.get(name)
def __getattr__(self, name):
if name.startswith("_"):
raise AttributeError(name)
if name in self._services:
return self._services[name]
raise AttributeError(f"No service registered: {name}")
def __contains__(self, name):
return name in self._services
def initialize_all(self, ctx):
"""Call ``initialize(ctx)`` on every service that supports it."""
for svc in self._services.values():
init = getattr(svc, "initialize", None)
if callable(init):
init(ctx)
def shutdown_all(self):
"""Call ``shutdown()`` on every service that supports it."""
for name, svc in self._services.items():
shutdown = getattr(svc, "shutdown", None)
if callable(shutdown):
try:
shutdown()
except Exception as e:
# Generic catch is somewhat acceptable during global teardown to ensure other services
# still get their shutdown called, but we must log it so we aren't swallowing shutdown errors silently.
import logging
logging.getLogger(__name__).error("Service %s failed during shutdown: %s", name, e)
@property
def service_names(self):
return list(self._services.keys())