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.
Files changed (82) hide show
  1. local/__init__.py +1 -0
  2. local/agents/__init__.py +0 -0
  3. local/agents/base_agent.py +76 -0
  4. local/agents/critic_actions.py +9 -0
  5. local/agents/critic_agent.py +139 -0
  6. local/agents/critic_states.py +9 -0
  7. local/agents/critic_transitions.py +33 -0
  8. local/agents/generator_actions.py +15 -0
  9. local/agents/generator_agent.py +571 -0
  10. local/agents/generator_states.py +13 -0
  11. local/agents/generator_transitions.py +53 -0
  12. local/agents/memory_agent.py +146 -0
  13. local/agents/memory_agent_actions.py +7 -0
  14. local/agents/memory_agent_states.py +7 -0
  15. local/agents/memory_agent_transitions.py +31 -0
  16. local/api/__init__.py +0 -0
  17. local/api/gateway.py +297 -0
  18. local/api/settings_api.py +76 -0
  19. local/api/static/assets/index-Cfu3Yfjs.js +77 -0
  20. local/api/static/assets/index-DCfLSdY6.css +1 -0
  21. local/api/static/index.html +13 -0
  22. local/api/ws_bridge.py +120 -0
  23. local/cli.py +85 -0
  24. local/config_loader.py +105 -0
  25. local/data_dir.py +38 -0
  26. local/defaults/bus.yaml +4 -0
  27. local/defaults/critic.yaml +32 -0
  28. local/defaults/documents.yaml +14 -0
  29. local/defaults/generator.yaml +38 -0
  30. local/defaults/location.yaml +10 -0
  31. local/defaults/memory.yaml +22 -0
  32. local/defaults/search_memory.yaml +18 -0
  33. local/defaults/semantic_scholar.yaml +5 -0
  34. local/defaults/system.yaml +6 -0
  35. local/defaults/web_fetch.yaml +11 -0
  36. local/defaults/web_search.yaml +15 -0
  37. local/protocol/__init__.py +0 -0
  38. local/protocol/envelope.py +50 -0
  39. local/protocol/subjects.py +62 -0
  40. local/run.py +205 -0
  41. local/runtime/__init__.py +0 -0
  42. local/runtime/proxy.py +31 -0
  43. local/services/__init__.py +0 -0
  44. local/services/conversation_service.py +220 -0
  45. local/services/document_service.py +427 -0
  46. local/services/memory_service.py +311 -0
  47. local/services/ollama_backend.py +111 -0
  48. local/services/reward_service.py +80 -0
  49. local/session/__init__.py +0 -0
  50. local/session/local_session.py +132 -0
  51. local/tools/__init__.py +0 -0
  52. local/tools/base_tool.py +132 -0
  53. local/tools/datetime_tool.py +72 -0
  54. local/tools/location_tool.py +164 -0
  55. local/tools/search_library_tool.py +152 -0
  56. local/tools/search_memory_tool.py +86 -0
  57. local/tools/semantic_scholar_tool.py +173 -0
  58. local/tools/web_fetch_tool.py +99 -0
  59. local/tools/web_search_tool.py +117 -0
  60. local/transport/__init__.py +0 -0
  61. local/transport/base.py +24 -0
  62. local/transport/bus.py +28 -0
  63. local/transport/bus_config.py +49 -0
  64. local/transport/zmq_pubsub.py +99 -0
  65. local/ui/__init__.py +0 -0
  66. local/ui/attachment_bar.py +134 -0
  67. local/ui/conversations_window.py +196 -0
  68. local/ui/critic_window.py +51 -0
  69. local/ui/documents_window.py +715 -0
  70. local/ui/generator_window.py +550 -0
  71. local/ui/main_window.py +1274 -0
  72. local/ui/memory_window.py +531 -0
  73. local/ui/monitor_app.py +272 -0
  74. local/ui/tool_panel.py +188 -0
  75. local/ui/tool_window.py +345 -0
  76. local/utils/__init__.py +0 -0
  77. local/utils/file_extract.py +61 -0
  78. local2-0.1.0.dist-info/METADATA +257 -0
  79. local2-0.1.0.dist-info/RECORD +82 -0
  80. local2-0.1.0.dist-info/WHEEL +4 -0
  81. local2-0.1.0.dist-info/entry_points.txt +2 -0
  82. local2-0.1.0.dist-info/licenses/LICENSE +21 -0
local/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """LoCAL2 package root."""
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,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class CriticAction(Enum):
5
+ RECEIVE = "receive"
6
+ START_GRADE = "start_grade"
7
+ PUBLISH = "publish"
8
+ FAIL = "fail"
9
+ RESET = "reset"
@@ -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,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class CriticState(Enum):
5
+ IDLE = "idle"
6
+ RECEIVING = "receiving"
7
+ GRADING = "grading"
8
+ PUBLISHING = "publishing"
9
+ ERROR = "error"
@@ -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