local2 0.1.0__py3-none-any.whl
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.
- local/__init__.py +1 -0
- local/agents/__init__.py +0 -0
- local/agents/base_agent.py +76 -0
- local/agents/critic_actions.py +9 -0
- local/agents/critic_agent.py +139 -0
- local/agents/critic_states.py +9 -0
- local/agents/critic_transitions.py +33 -0
- local/agents/generator_actions.py +15 -0
- local/agents/generator_agent.py +571 -0
- local/agents/generator_states.py +13 -0
- local/agents/generator_transitions.py +53 -0
- local/agents/memory_agent.py +146 -0
- local/agents/memory_agent_actions.py +7 -0
- local/agents/memory_agent_states.py +7 -0
- local/agents/memory_agent_transitions.py +31 -0
- local/api/__init__.py +0 -0
- local/api/gateway.py +297 -0
- local/api/settings_api.py +76 -0
- local/api/static/assets/index-Cfu3Yfjs.js +77 -0
- local/api/static/assets/index-DCfLSdY6.css +1 -0
- local/api/static/index.html +13 -0
- local/api/ws_bridge.py +120 -0
- local/cli.py +85 -0
- local/config_loader.py +105 -0
- local/data_dir.py +38 -0
- local/defaults/bus.yaml +4 -0
- local/defaults/critic.yaml +32 -0
- local/defaults/documents.yaml +14 -0
- local/defaults/generator.yaml +38 -0
- local/defaults/location.yaml +10 -0
- local/defaults/memory.yaml +22 -0
- local/defaults/search_memory.yaml +18 -0
- local/defaults/semantic_scholar.yaml +5 -0
- local/defaults/system.yaml +6 -0
- local/defaults/web_fetch.yaml +11 -0
- local/defaults/web_search.yaml +15 -0
- local/protocol/__init__.py +0 -0
- local/protocol/envelope.py +50 -0
- local/protocol/subjects.py +62 -0
- local/run.py +205 -0
- local/runtime/__init__.py +0 -0
- local/runtime/proxy.py +31 -0
- local/services/__init__.py +0 -0
- local/services/conversation_service.py +220 -0
- local/services/document_service.py +427 -0
- local/services/memory_service.py +311 -0
- local/services/ollama_backend.py +111 -0
- local/services/reward_service.py +80 -0
- local/session/__init__.py +0 -0
- local/session/local_session.py +132 -0
- local/tools/__init__.py +0 -0
- local/tools/base_tool.py +132 -0
- local/tools/datetime_tool.py +72 -0
- local/tools/location_tool.py +164 -0
- local/tools/search_library_tool.py +152 -0
- local/tools/search_memory_tool.py +86 -0
- local/tools/semantic_scholar_tool.py +173 -0
- local/tools/web_fetch_tool.py +99 -0
- local/tools/web_search_tool.py +117 -0
- local/transport/__init__.py +0 -0
- local/transport/base.py +24 -0
- local/transport/bus.py +28 -0
- local/transport/bus_config.py +49 -0
- local/transport/zmq_pubsub.py +99 -0
- local/ui/__init__.py +0 -0
- local/ui/attachment_bar.py +134 -0
- local/ui/conversations_window.py +196 -0
- local/ui/critic_window.py +51 -0
- local/ui/documents_window.py +715 -0
- local/ui/generator_window.py +550 -0
- local/ui/main_window.py +1274 -0
- local/ui/memory_window.py +531 -0
- local/ui/monitor_app.py +272 -0
- local/ui/tool_panel.py +188 -0
- local/ui/tool_window.py +345 -0
- local/utils/__init__.py +0 -0
- local/utils/file_extract.py +61 -0
- local2-0.1.0.dist-info/METADATA +257 -0
- local2-0.1.0.dist-info/RECORD +82 -0
- local2-0.1.0.dist-info/WHEEL +4 -0
- local2-0.1.0.dist-info/entry_points.txt +2 -0
- local2-0.1.0.dist-info/licenses/LICENSE +21 -0
local/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""LoCAL2 package root."""
|
local/agents/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""BaseAgent — common behaviour for all LoCAL2 system-triggered agents."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, ClassVar
|
|
8
|
+
|
|
9
|
+
from local.protocol.envelope import MessageEnvelope
|
|
10
|
+
from local.protocol.subjects import AGENT_TRANSITION
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseAgent(ABC):
|
|
16
|
+
"""Abstract base for all system-triggered LoCAL2 agents.
|
|
17
|
+
|
|
18
|
+
Subclasses must:
|
|
19
|
+
- declare ``AGENT_ID`` as a class variable
|
|
20
|
+
- set ``self._pub``, ``self._sub``, and ``self._sm`` in ``__init__``
|
|
21
|
+
- implement ``_dispatch()`` to route envelopes to their handlers
|
|
22
|
+
|
|
23
|
+
``run()`` provides the standard receive loop. ``GeneratorAgent`` overrides
|
|
24
|
+
it for startup sequencing; other agents inherit it directly.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
AGENT_ID: ClassVar[str]
|
|
28
|
+
_pub: Any
|
|
29
|
+
_sub: Any
|
|
30
|
+
_sm: Any
|
|
31
|
+
|
|
32
|
+
def run(self) -> None:
|
|
33
|
+
"""Block on the bus and dispatch each envelope; log receive errors."""
|
|
34
|
+
logger.info("%s ready", self.AGENT_ID)
|
|
35
|
+
while True:
|
|
36
|
+
try:
|
|
37
|
+
envelope = self._sub.receive()
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
logger.error("%s: receive error: %s", self.__class__.__name__, exc)
|
|
40
|
+
continue
|
|
41
|
+
self._dispatch(envelope)
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def _dispatch(self, envelope: MessageEnvelope) -> None:
|
|
45
|
+
"""Route an incoming envelope to the appropriate handler."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def _do_transition(self, action: Any) -> None:
|
|
49
|
+
"""Execute a state machine transition and publish AGENT_TRANSITION.
|
|
50
|
+
|
|
51
|
+
Wrapped in try/except — transition logging must never propagate and
|
|
52
|
+
interrupt the agent's main work. Calls ``_after_transition()`` after
|
|
53
|
+
a successful transition so subclasses can add side-effects (e.g.
|
|
54
|
+
``GeneratorAgent`` uses it to publish a status snapshot).
|
|
55
|
+
"""
|
|
56
|
+
from_state = self._sm.state
|
|
57
|
+
to_state = self._sm.transition(action)
|
|
58
|
+
try:
|
|
59
|
+
self._pub.publish(MessageEnvelope.create(
|
|
60
|
+
message_type="agent_transition",
|
|
61
|
+
subject=AGENT_TRANSITION,
|
|
62
|
+
sender_id=self.AGENT_ID,
|
|
63
|
+
payload={
|
|
64
|
+
"agent": self.AGENT_ID,
|
|
65
|
+
"from": from_state.value,
|
|
66
|
+
"action": action.value,
|
|
67
|
+
"to": to_state.value,
|
|
68
|
+
},
|
|
69
|
+
correlation_id=str(uuid.uuid4()),
|
|
70
|
+
))
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
self._after_transition()
|
|
74
|
+
|
|
75
|
+
def _after_transition(self) -> None:
|
|
76
|
+
"""Hook called after every transition. Default: no-op."""
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""CriticAgent — post-generation quality observer.
|
|
2
|
+
|
|
3
|
+
Subscribes to response.generation. For each answer, calls Prometheus via
|
|
4
|
+
OllamaBackend to produce an absolute quality score (1–5) and a feedback
|
|
5
|
+
string. Publishes critique.result.
|
|
6
|
+
|
|
7
|
+
Never blocks or raises: on Prometheus failure or score parse failure,
|
|
8
|
+
publishes critique.result with score=None so downstream consumers
|
|
9
|
+
(MemoryAgent, UI) can treat null as "not graded" and continue normally.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from local.agents.base_agent import BaseAgent
|
|
16
|
+
from local.agents.critic_actions import CriticAction
|
|
17
|
+
from local.agents.critic_states import CriticState
|
|
18
|
+
from local.agents.critic_transitions import CriticStateMachine
|
|
19
|
+
from local.config_loader import get_config
|
|
20
|
+
from local.protocol.envelope import MessageEnvelope
|
|
21
|
+
from local.protocol.subjects import CRITIQUE, RESPONSE_GENERATION
|
|
22
|
+
from local.services.ollama_backend import OllamaBackend
|
|
23
|
+
from local.transport.bus_config import make_participant_bus
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CriticAgent(BaseAgent):
|
|
29
|
+
"""Post-generation quality observer.
|
|
30
|
+
|
|
31
|
+
Grades every non-tool-call answer with an absolute score (1–5) via
|
|
32
|
+
Prometheus. Never raises — publishes ``score=None`` on any Prometheus
|
|
33
|
+
failure so downstream consumers (MemoryAgent, UI) can treat null as
|
|
34
|
+
"not graded" and continue normally.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
AGENT_ID = "critic"
|
|
38
|
+
|
|
39
|
+
def __init__(self, llm: OllamaBackend | None = None) -> None:
|
|
40
|
+
"""Initialize the CriticAgent.
|
|
41
|
+
|
|
42
|
+
Config keys read from ``config/critic.yaml``: ``model``, ``rubric``,
|
|
43
|
+
``grade_prompt``, ``pairwise_prompt``, ``pairwise_buffer_max``,
|
|
44
|
+
``num_ctx``, ``temperature``, ``grade_timeout``.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
llm: Injected for testing; defaults to an ``OllamaBackend`` built
|
|
48
|
+
from config.
|
|
49
|
+
"""
|
|
50
|
+
cfg = get_config("critic")
|
|
51
|
+
model: str = cfg.get("model", "prometheus:7b")
|
|
52
|
+
self._rubric: str = cfg.get("rubric", "")
|
|
53
|
+
self._grade_prompt: str = cfg.get("grade_prompt", "").strip()
|
|
54
|
+
self._options: dict = {
|
|
55
|
+
"num_ctx": cfg.get("num_ctx", 4096),
|
|
56
|
+
"temperature": cfg.get("temperature", 0.0),
|
|
57
|
+
}
|
|
58
|
+
timeout: int = cfg.get("grade_timeout", 30)
|
|
59
|
+
self._llm = llm or OllamaBackend(model=model, agent_name=self.AGENT_ID, timeout=timeout)
|
|
60
|
+
self._pub, self._sub = make_participant_bus([RESPONSE_GENERATION])
|
|
61
|
+
self._sm = CriticStateMachine()
|
|
62
|
+
|
|
63
|
+
def _dispatch(self, envelope: MessageEnvelope) -> None:
|
|
64
|
+
if envelope.subject == RESPONSE_GENERATION:
|
|
65
|
+
try:
|
|
66
|
+
self._handle_generation(envelope)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logger.error("CriticAgent: unhandled error: %s", exc, exc_info=True)
|
|
69
|
+
if self._sm.state != CriticState.IDLE:
|
|
70
|
+
self._do_transition(CriticAction.FAIL)
|
|
71
|
+
self._do_transition(CriticAction.RESET)
|
|
72
|
+
|
|
73
|
+
def _handle_generation(self, envelope: MessageEnvelope) -> None:
|
|
74
|
+
payload = envelope.payload
|
|
75
|
+
query: str = payload.get("query", "").strip()
|
|
76
|
+
answer: str = payload.get("answer", "").strip()
|
|
77
|
+
session_id: str = payload.get("session_id") or ""
|
|
78
|
+
query_id: str = payload.get("query_id") or ""
|
|
79
|
+
correlation_id: str = envelope.correlation_id or query_id
|
|
80
|
+
|
|
81
|
+
if not query or not answer or payload.get("error"):
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if payload.get("tool_calls"):
|
|
85
|
+
logger.debug("CriticAgent: skipping grade — tool calls present")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# -- Absolute grade --------------------------------------------------
|
|
89
|
+
self._do_transition(CriticAction.RECEIVE)
|
|
90
|
+
self._do_transition(CriticAction.START_GRADE)
|
|
91
|
+
|
|
92
|
+
score, feedback = self._grade(query, answer)
|
|
93
|
+
|
|
94
|
+
if score is None:
|
|
95
|
+
self._do_transition(CriticAction.FAIL)
|
|
96
|
+
else:
|
|
97
|
+
self._do_transition(CriticAction.PUBLISH)
|
|
98
|
+
|
|
99
|
+
self._pub.publish(MessageEnvelope.create(
|
|
100
|
+
message_type="critique",
|
|
101
|
+
subject=CRITIQUE,
|
|
102
|
+
sender_id=self.AGENT_ID,
|
|
103
|
+
payload={
|
|
104
|
+
"score": score,
|
|
105
|
+
"feedback": feedback,
|
|
106
|
+
"query": query,
|
|
107
|
+
"answer": answer,
|
|
108
|
+
"session_id": session_id,
|
|
109
|
+
"query_id": query_id,
|
|
110
|
+
},
|
|
111
|
+
correlation_id=correlation_id,
|
|
112
|
+
metadata={"session_id": session_id},
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
self._do_transition(CriticAction.RESET)
|
|
116
|
+
|
|
117
|
+
def _grade(self, query: str, answer: str) -> tuple[int | None, str]:
|
|
118
|
+
"""Call Prometheus absolute grading. Returns (score_or_None, feedback_text)."""
|
|
119
|
+
prompt = self._grade_prompt.format(query=query, answer=answer, rubric=self._rubric)
|
|
120
|
+
text, _ = self._llm.chat(
|
|
121
|
+
[{"role": "user", "content": prompt}],
|
|
122
|
+
options=self._options,
|
|
123
|
+
)
|
|
124
|
+
if not text:
|
|
125
|
+
logger.warning("CriticAgent: Prometheus returned empty response")
|
|
126
|
+
return None, ""
|
|
127
|
+
|
|
128
|
+
m = re.search(r'\[RESULT\]\s*([1-5])', text)
|
|
129
|
+
score = int(m.group(1)) if m else None
|
|
130
|
+
if score is None:
|
|
131
|
+
logger.warning("CriticAgent: could not parse score from: %r", text[:120])
|
|
132
|
+
|
|
133
|
+
feedback = re.sub(r'\s*\[RESULT\].*$', '', text, flags=re.DOTALL).strip()
|
|
134
|
+
if feedback.lower().startswith("feedback:"):
|
|
135
|
+
feedback = feedback[len("feedback:"):].strip()
|
|
136
|
+
|
|
137
|
+
return score, feedback
|
|
138
|
+
|
|
139
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""CriticAgent transition table and StateMachine executor."""
|
|
2
|
+
from local.agents.critic_actions import CriticAction
|
|
3
|
+
from local.agents.critic_states import CriticState
|
|
4
|
+
|
|
5
|
+
TRANSITIONS: dict[tuple[CriticState, CriticAction], CriticState] = {
|
|
6
|
+
(CriticState.IDLE, CriticAction.RECEIVE): CriticState.RECEIVING,
|
|
7
|
+
(CriticState.RECEIVING, CriticAction.START_GRADE): CriticState.GRADING,
|
|
8
|
+
(CriticState.GRADING, CriticAction.PUBLISH): CriticState.PUBLISHING,
|
|
9
|
+
(CriticState.GRADING, CriticAction.FAIL): CriticState.ERROR,
|
|
10
|
+
(CriticState.PUBLISHING, CriticAction.RESET): CriticState.IDLE,
|
|
11
|
+
(CriticState.ERROR, CriticAction.RESET): CriticState.IDLE,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CriticStateMachine:
|
|
16
|
+
"""Enforces the critic transition table. Raises on illegal transitions."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._state = CriticState.IDLE
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def state(self) -> CriticState:
|
|
23
|
+
return self._state
|
|
24
|
+
|
|
25
|
+
def transition(self, action: CriticAction) -> CriticState:
|
|
26
|
+
key = (self._state, action)
|
|
27
|
+
next_state = TRANSITIONS.get(key)
|
|
28
|
+
if next_state is None:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Illegal transition: state={self._state.value!r} action={action.value!r}"
|
|
31
|
+
)
|
|
32
|
+
self._state = next_state
|
|
33
|
+
return self._state
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""GeneratorAgent action definitions."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GeneratorAction(Enum):
|
|
7
|
+
RECEIVE = "receive" # query.received consumed
|
|
8
|
+
START_GENERATION = "start_generation" # begin ollama.chat()
|
|
9
|
+
DISPATCH_TOOL = "dispatch_tool" # tool_calls present, publishing tool.request.*
|
|
10
|
+
AWAIT_RESULT = "await_result" # tool.request.* published, blocking on tool.result.*
|
|
11
|
+
TOOL_RESULT = "tool_result" # tool.result.* received, resume loop
|
|
12
|
+
TOOL_TIMEOUT = "tool_timeout" # no tool.result.* within deadline, inject error
|
|
13
|
+
PUBLISH = "publish" # no tool calls, answer ready
|
|
14
|
+
RESET = "reset" # response.generation published, back to idle
|
|
15
|
+
FAIL = "fail" # unrecoverable error
|