diff --git a/README.md b/README.md index 11b3a28c..a338de10 100644 --- a/README.md +++ b/README.md @@ -13,38 +13,22 @@ The engine is designed for multi-runtime local AI setups where the main reasonin Runtime memory can now influence conversation strategy, not just store facts. JIN tracks interaction patterns during a session and can adjust replies when the conversation starts looping instead of blindly restarting the same exchange. - + - + ## Capabilities -- FastAPI application with HTML UI at `/` and provider status at `/api/status`. -- WebSocket chat endpoint at `/ws/chat` with streaming output, logs, telemetry, and cancellation. -- OpenAI-compatible runtime clients for `/v1/chat/completions` and `/v1/models`. -- Separate runtime roles: `brain`, `service`, and `translator`. -- Optional `USE_SERVICE_AS_BRAIN` mode for running without a dedicated brain provider. -- Model-driven runtime actions, currently including web search requests emitted by the brain and executed by the runtime. -- Search result injection through trusted runtime context instead of raw chat history. -- Streaming lifecycle events for message start, thinking chunks, content chunks, completion, and errors. -- Reasoning/thinking chunks rendered separately from final assistant content. -- Runtime telemetry for model IDs, context windows, token usage, provider status, and runtime errors. -- Live runtime memory: a compact in-RAM state updated by the service model after each completed turn. -- Runtime memory panel in the right sidebar, showing the current memory state without XML tags. -- Runtime memory snapshots with arrow navigation for reviewing how the session state evolved. -- Runtime memory diff highlighting for new keys, changed keys, new values, and changed values. -- Runtime pattern memory that detects repeated interaction shapes and feeds behavior signals back into the brain prompt. -- Conversation activity signaling that helps JIN notice when an exchange is cooling down or stuck. -- No-signal and zero-diff handling so repeated empty turns can change response strategy instead of producing another generic prompt. -- Summarizer request logging with inspectable payloads for debugging what memory models received. -- Memory update animation: the settings panel glows during summarization and changed memory lines briefly flash before settling back into a unified state. -- Separate L1 and L2 memory update indicators for factual memory and pattern memory activity. -- Interrupted turn memory handling: aborted or incomplete responses are marked as unresolved state. -- Stream validation for repeated word loops, repeated sentences, repeated paragraphs, and leading HTML artifacts. -- Abort support that cancels the active task, closes active provider streams, and records interrupted memory. -- Agent runtime path for Cyrillic input: planner, internal translator, brain, validator. -- Direct brain route for non-Cyrillic input. -- Keyboard-first input: Enter sends, Ctrl/Shift+Enter inserts a newline, and the input field becomes the stop control during generation. +- A chat room that feels alive: answers stream in as they are written, thinking stays visually separate from the final reply, and you can stop a generation the moment it drifts. +- A visible short-term memory: JIN keeps a compact sense of what this session is about, what changed, and what still feels unresolved. +- A memory timeline you can inspect: step through snapshots and see which facts or patterns were added instead of guessing what the assistant remembered. +- A calmer loop breaker: when the conversation starts repeating itself, JIN can notice the pattern and change strategy instead of giving the same polite answer again. +- A built-in search move: the model can ask the runtime to search, then answer from trusted results without dumping raw tool syntax into the chat. +- A multilingual path that stays out of your way: Cyrillic input can be translated internally while the visible conversation remains natural. +- A right sidebar that shows what is happening under the hood: model status, context pressure, token usage, runtime memory, and live logs are there when you want them. +- A keyboard-first writing flow: Enter sends, Ctrl/Shift+Enter adds a newline, and the input box turns into the stop control while JIN is working. +- A local-first setup for people who run their own models: use separate brain, service, and translator runtimes, or collapse to one service model when you want a simpler setup. +- A deploy-friendly configuration story: use a local `config.py` while experimenting, then switch to environment variables when you are ready to run it somewhere more serious. ## Architecture @@ -54,7 +38,7 @@ Browser UI v FastAPI app.py | - +-- GET / -> templates/index.html + +-- GET / -> ui/templates/index.html +-- GET /api/status -> provider availability and runtime metadata +-- WS /ws/chat -> streaming chat transport | @@ -127,18 +111,15 @@ This gives JIN observable short-term memory and behavior adaptation without intr |-- websocket.py # WebSocket runtime loop and cancellation |-- websocket_logger.py # JSON logs for the UI console |-- config.example.py # Runtime configuration template +|-- config_loader.py # Local config module loader +|-- app_settings.py # Typed settings wrapper |-- package.json # Local command shortcuts |-- requirements.txt # Pinned Python dependencies |-- .github/workflows/ # GitHub Actions CI -|-- agents/ # Agent runtime and nodes +|-- agent/ # Agent runtime, state, router, and nodes |-- clients/ # Runtime client builders and provider helpers -|-- contracts/ # Runtime context contracts -|-- emitter/ # WebSocket JSON emitter -|-- memory/ # Memory and runtime state abstractions -|-- runtime/ # Runtime client, context, stream, registry -|-- settings/ # Config loader and typed settings wrapper -|-- static/ # Browser JavaScript and README assets -|-- templates/ # HTML UI +|-- runtime/ # Runtime client, context, contracts, memory, stream, registry +|-- ui/ # HTML templates, browser JavaScript, and README assets |-- tests/ # Unit and optional model integration tests `-- utils/ # Stream, telemetry, language, token, error helpers ``` @@ -206,6 +187,17 @@ http://127.0.0.1:8000 `config.py` defines model providers, model IDs, request limits, context windows, and generation parameters. It is intentionally ignored by Git because it contains local runtime addresses. When `config.py` is absent, the app falls back to `config.example.py`, which keeps CI and basic tests runnable without private local settings. +For deployment, every uppercase option can also be provided through environment variables. Environment values override `config.py` and `config.example.py`. Both plain names and `JIN_`-prefixed names are supported: + +```bash +BRAIN_API_BASE=http://brain-host:1234 +JIN_SERVICE_MODEL_UID=service-model +USE_SERVICE_AS_BRAIN=true +SEARCH_TIMEOUT=20.0 +``` + +Plain names take priority over prefixed names when both are set. Boolean env values accept `1`, `true`, `yes`, `on`, `0`, `false`, `no`, and `off`. + ```python USE_SERVICE_AS_BRAIN = False @@ -354,12 +346,12 @@ Runtime memory update: The UI is served directly by FastAPI: -- `templates/index.html` renders the shell. -- `static/socket.js` handles WebSocket connection, send, abort, and stream events. -- `static/chat.js` renders normal and streaming messages. -- `static/status.js` updates provider online/offline indicators. -- `static/telemetry.js` updates runtime status, context usage, runtime memory snapshots, and memory diff highlighting. -- `static/logger.js` renders the runtime console. -- `static/dragdrop.js` handles attachment UI state. +- `ui/templates/index.html` renders the shell. +- `ui/static/js/socket.js` handles WebSocket connection, send, abort, and stream events. +- `ui/static/js/chat.js` renders normal and streaming messages. +- `ui/static/js/status.js` updates provider online/offline indicators. +- `ui/static/js/telemetry.js` updates runtime status, context usage, runtime memory snapshots, and memory diff highlighting. +- `ui/static/js/logger.js` renders the runtime console. +- `ui/static/js/dragdrop.js` handles attachment UI state. The frontend uses vanilla JavaScript and Tailwind from CDN. The current input behavior is keyboard-first: Enter sends, Ctrl/Shift+Enter inserts a newline, and the whole input field becomes a red stop control while a generation is active. diff --git a/agent/__init__.py b/agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agent/nodes/__init__.py b/agent/nodes/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/agent/nodes/__init__.py @@ -0,0 +1 @@ + diff --git a/agents/base_node.py b/agent/nodes/base.py similarity index 100% rename from agents/base_node.py rename to agent/nodes/base.py diff --git a/agents/brain_node.py b/agent/nodes/brain.py similarity index 98% rename from agents/brain_node.py rename to agent/nodes/brain.py index fd804c3d..98477d52 100644 --- a/agents/brain_node.py +++ b/agent/nodes/brain.py @@ -1,6 +1,6 @@ -from agents.base_node import BaseNode +from agent.nodes.base import BaseNode -from runtime.runtime_stream import ( +from runtime.stream import ( RuntimeStream, ) diff --git a/agents/planner_node.py b/agent/nodes/planner.py similarity index 92% rename from agents/planner_node.py rename to agent/nodes/planner.py index e3c3adaa..a98f8a8b 100644 --- a/agents/planner_node.py +++ b/agent/nodes/planner.py @@ -1,9 +1,9 @@ -from agents.base_node import BaseNode +from agent.nodes.base import BaseNode from utils.language import ( contains_cyrillic, ) -from settings.config_loader import ( +from config_loader import ( config, ) diff --git a/agents/translation_node.py b/agent/nodes/translation.py similarity index 95% rename from agents/translation_node.py rename to agent/nodes/translation.py index b8ead6a9..ced480df 100644 --- a/agents/translation_node.py +++ b/agent/nodes/translation.py @@ -1,10 +1,10 @@ -from agents.base_node import BaseNode +from agent.nodes.base import BaseNode from clients.translation_client import ( translate, ) -from settings.app_settings import ( +from app_settings import ( settings, ) diff --git a/agents/validation_node.py b/agent/nodes/validation.py similarity index 95% rename from agents/validation_node.py rename to agent/nodes/validation.py index 947b11a6..02d3bc3d 100644 --- a/agents/validation_node.py +++ b/agent/nodes/validation.py @@ -1,10 +1,10 @@ -from agents.base_node import BaseNode +from agent.nodes.base import BaseNode -from runtime.runtime_stream import ( +from runtime.stream import ( RuntimeStream, ) -from settings.app_settings import ( +from app_settings import ( settings, ) diff --git a/agents/router.py b/agent/router.py similarity index 100% rename from agents/router.py rename to agent/router.py diff --git a/agents/agent_runtime.py b/agent/runtime.py similarity index 83% rename from agents/agent_runtime.py rename to agent/runtime.py index 2edc7cb1..891cb3b7 100644 --- a/agents/agent_runtime.py +++ b/agent/runtime.py @@ -1,20 +1,20 @@ -from agents.planner_node import ( +from agent.nodes.planner import ( PlannerNode, ) -from agents.translation_node import ( +from agent.nodes.translation import ( TranslationNode, ) -from agents.validation_node import ( +from agent.nodes.validation import ( ValidationNode, ) -from agents.router import ( +from agent.router import ( Router, ) -from agents.brain_node import ( +from agent.nodes.brain import ( BrainNode, ) diff --git a/agents/agent_state.py b/agent/state.py similarity index 100% rename from agents/agent_state.py rename to agent/state.py diff --git a/app.py b/app.py index 735302f4..bd60626c 100644 --- a/app.py +++ b/app.py @@ -20,7 +20,7 @@ import asyncio import httpx -from settings.config_loader import ( +from config_loader import ( config, ) @@ -32,11 +32,11 @@ websocket_router, ) -from clients.clients_registry import ( +from clients.registry import ( build_clients, ) -from memory.runtime_state import ( +from runtime.state import ( RUNTIME_MEMORY_SUMMARIZER_LABEL, ) @@ -92,12 +92,12 @@ async def lifespan(application: FastAPI): ) templates = Jinja2Templates( - directory="templates", + directory="ui/templates", ) app.mount( "/static", - StaticFiles(directory="static"), + StaticFiles(directory="ui/static"), name="static", ) diff --git a/settings/app_settings.py b/app_settings.py similarity index 98% rename from settings/app_settings.py rename to app_settings.py index be4d5e12..9563128b 100644 --- a/settings/app_settings.py +++ b/app_settings.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from settings.config_loader import ( +from config_loader import ( config, ) diff --git a/clients/__init__.py b/clients/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clients/brain_client.py b/clients/brain_client.py index 73ef34f2..b57a78d0 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -3,10 +3,10 @@ from xml.etree import ElementTree from xml.sax.saxutils import escape -from settings.config_loader import ( +from config_loader import ( config, ) -from contracts.context_contract import ( +from runtime.context_contract import ( ContextContract, DEEP_THOUGHT_ACTION, RUNTIME_ACTION_DEEP_THOUGHT, @@ -14,7 +14,7 @@ SEARCH_ACTION_TEMPLATE, ) -from utils.errors import ( +from clients.errors import ( format_client_error, ) @@ -23,7 +23,7 @@ ask_service_model_stream, ) -from utils.response_extractor import ( +from clients.response_extractor import ( ResponseExtractor, ) diff --git a/utils/errors.py b/clients/errors.py similarity index 100% rename from utils/errors.py rename to clients/errors.py diff --git a/clients/clients_registry.py b/clients/registry.py similarity index 94% rename from clients/clients_registry.py rename to clients/registry.py index 5bcef9fe..6e9ec00d 100644 --- a/clients/clients_registry.py +++ b/clients/registry.py @@ -1,6 +1,6 @@ -from settings.app_settings import settings +from app_settings import settings -from runtime.runtime_client import ( +from runtime.client import ( RuntimeClient, ) diff --git a/utils/response_extractor.py b/clients/response_extractor.py similarity index 100% rename from utils/response_extractor.py rename to clients/response_extractor.py diff --git a/clients/search_client.py b/clients/search_client.py index b6a97a87..d5a5589f 100644 --- a/clients/search_client.py +++ b/clients/search_client.py @@ -3,11 +3,11 @@ import httpx -from settings.app_settings import ( +from app_settings import ( settings, ) -from utils.search import ( +from clients.search_provider import ( serper_search, ) diff --git a/utils/search.py b/clients/search_provider.py similarity index 100% rename from utils/search.py rename to clients/search_provider.py diff --git a/clients/translation_client.py b/clients/translation_client.py index 1b07e579..0cc7dbd5 100644 --- a/clients/translation_client.py +++ b/clients/translation_client.py @@ -1,9 +1,9 @@ import asyncio -from settings.config_loader import ( +from config_loader import ( config, ) -from utils.errors import ( +from clients.errors import ( format_client_error, ) @@ -11,7 +11,7 @@ translation_token_limit, ) -from utils.response_extractor import ( +from clients.response_extractor import ( ResponseExtractor, ) diff --git a/config_loader.py b/config_loader.py new file mode 100644 index 00000000..708b0845 --- /dev/null +++ b/config_loader.py @@ -0,0 +1,176 @@ +from importlib import import_module +from importlib import util +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +ENV_PREFIX = "JIN_" + + +def parse_env_value( + *, + name: str, + raw_value: str, + current_value, +): + + if isinstance(current_value, bool): + + normalized = ( + raw_value + .strip() + .lower() + ) + + if normalized in { + "1", + "true", + "yes", + "on", + }: + return True + + if normalized in { + "0", + "false", + "no", + "off", + }: + return False + + raise ValueError( + f"Invalid boolean env value for {name}: {raw_value!r}" + ) + + if isinstance(current_value, int): + return int( + raw_value + ) + + if isinstance(current_value, float): + return float( + raw_value + ) + + return raw_value + + +def get_env_override( + name: str, +) -> str | None: + + if name in os.environ: + return os.environ[name] + + prefixed_name = f"{ENV_PREFIX}{name}" + + return os.environ.get( + prefixed_name + ) + + +def apply_env_overrides( + config_module, +): + + for name in dir(config_module): + + if not name.isupper(): + continue + + raw_value = get_env_override( + name + ) + + if raw_value is None: + continue + + current_value = getattr( + config_module, + name, + ) + + setattr( + config_module, + name, + parse_env_value( + name=name, + raw_value=raw_value, + current_value=current_value, + ), + ) + + return config_module + + +def load_config_from_path( + path: Path, +): + + spec = util.spec_from_file_location( + path.stem, + path, + ) + + if ( + spec is None + or spec.loader is None + ): + raise RuntimeError( + f"Unable to load config from {path}" + ) + + module = util.module_from_spec( + spec + ) + + spec.loader.exec_module( + module + ) + + return module + + +def load_config_module( + *, + config_path: Path | None = None, + example_path: Path | None = None, +): + + if config_path is None: + + try: + return apply_env_overrides( + import_module( + "config" + ) + ) + + except ModuleNotFoundError as error: + + if error.name != "config": + raise + + else: + + if config_path.exists(): + return apply_env_overrides( + load_config_from_path( + config_path + ) + ) + + fallback_path = ( + example_path + or ROOT / "config.example.py" + ) + + return apply_env_overrides( + load_config_from_path( + fallback_path + ) + ) + + +config = load_config_module() diff --git a/emitter/runtime_emitter.py b/emitter/runtime_emitter.py deleted file mode 100644 index b7f6a958..00000000 --- a/emitter/runtime_emitter.py +++ /dev/null @@ -1,17 +0,0 @@ -class RuntimeEmitter: - - def __init__( - self, - websocket, - ): - - self.websocket = websocket - - async def emit( - self, - payload: dict, - ): - - await self.websocket.send_json( - payload - ) \ No newline at end of file diff --git a/package.json b/package.json index c26b4840..28b071ec 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,6 @@ "scripts": { "test": "python -m unittest discover -s tests", "tests": "npm test", - "translation_tests": "python tools/run_translation_tests.py" + "translation_tests": "python tests/run_translation_tests.py" } } diff --git a/runtime/__init__.py b/runtime/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/runtime/runtime_client.py b/runtime/client.py similarity index 99% rename from runtime/runtime_client.py rename to runtime/client.py index e1799833..7a21b02a 100644 --- a/runtime/runtime_client.py +++ b/runtime/client.py @@ -4,13 +4,13 @@ import httpx -from settings.app_settings import settings +from app_settings import settings from utils.urls import ( join_url, ) -from utils.response_extractor import ( +from clients.response_extractor import ( ResponseExtractor, ) diff --git a/runtime/runtime_context.py b/runtime/context.py similarity index 85% rename from runtime/runtime_context.py rename to runtime/context.py index f12909cf..c8e07bf3 100644 --- a/runtime/runtime_context.py +++ b/runtime/context.py @@ -2,15 +2,30 @@ from websocket_logger import WebSocketLogger -from emitter.runtime_emitter import ( - RuntimeEmitter, -) - -from memory.message_memory import ( +from runtime.memory import ( DEFAULT_RUNTIME_MEMORY, ) +class RuntimeEmitter: + + def __init__( + self, + websocket, + ): + + self.websocket = websocket + + async def emit( + self, + payload: dict, + ): + + await self.websocket.send_json( + payload + ) + + @dataclass class RuntimeContext: diff --git a/contracts/context_contract.py b/runtime/context_contract.py similarity index 100% rename from contracts/context_contract.py rename to runtime/context_contract.py diff --git a/memory/message_memory.py b/runtime/memory.py similarity index 99% rename from memory/message_memory.py rename to runtime/memory.py index b74eb703..5b73b183 100644 --- a/memory/message_memory.py +++ b/runtime/memory.py @@ -7,16 +7,16 @@ from clients.service_client import ( ask_service_model, ) -from memory.runtime_state import ( +from runtime.state import ( RUNTIME_MEMORY_SUMMARIZER_RUNTIME_ID, ) -from settings.config_loader import ( +from config_loader import ( config, ) -from utils.response_extractor import ( +from clients.response_extractor import ( ResponseExtractor, ) -from utils.runtime_state_sync import ( +from runtime.state_sync import ( refresh_runtime_state, ) from utils.tokens import ( diff --git a/runtime/registry.py b/runtime/registry.py new file mode 100644 index 00000000..be109f7d --- /dev/null +++ b/runtime/registry.py @@ -0,0 +1,3 @@ +from runtime.state import RuntimeState + +runtime_state = RuntimeState() diff --git a/runtime/runtime_registry.py b/runtime/runtime_registry.py deleted file mode 100644 index d549e06f..00000000 --- a/runtime/runtime_registry.py +++ /dev/null @@ -1,3 +0,0 @@ -from memory.runtime_state import RuntimeState - -runtime_state = RuntimeState() \ No newline at end of file diff --git a/memory/runtime_state.py b/runtime/state.py similarity index 98% rename from memory/runtime_state.py rename to runtime/state.py index aa54ec94..fbb0ae11 100644 --- a/memory/runtime_state.py +++ b/runtime/state.py @@ -1,4 +1,4 @@ -from settings.app_settings import settings +from app_settings import settings UNCHANGED = object() diff --git a/utils/runtime_state_sync.py b/runtime/state_sync.py similarity index 90% rename from utils/runtime_state_sync.py rename to runtime/state_sync.py index 004402e4..45de1f8a 100644 --- a/utils/runtime_state_sync.py +++ b/runtime/state_sync.py @@ -1,12 +1,12 @@ -from memory.runtime_state import ( +from runtime.state import ( UNCHANGED, ) -from runtime.runtime_registry import ( +from runtime.registry import ( runtime_state, ) -from utils.telemetry import ( +from runtime.telemetry import ( send_telemetry, ) diff --git a/runtime/runtime_stream.py b/runtime/stream.py similarity index 99% rename from runtime/runtime_stream.py rename to runtime/stream.py index 648196c4..375bf443 100644 --- a/runtime/runtime_stream.py +++ b/runtime/stream.py @@ -4,7 +4,7 @@ import httpx -from utils.runtime_state_sync import ( +from runtime.state_sync import ( refresh_runtime_state, ) diff --git a/utils/telemetry.py b/runtime/telemetry.py similarity index 85% rename from utils/telemetry.py rename to runtime/telemetry.py index 3ed3ffa0..1b3273d4 100644 --- a/utils/telemetry.py +++ b/runtime/telemetry.py @@ -1,4 +1,4 @@ -from runtime.runtime_registry import ( +from runtime.registry import ( runtime_state, ) diff --git a/settings/config_loader.py b/settings/config_loader.py deleted file mode 100644 index 9dda54a0..00000000 --- a/settings/config_loader.py +++ /dev/null @@ -1,72 +0,0 @@ -from importlib import import_module -from importlib import util -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] - - -def load_config_from_path( - path: Path, -): - - spec = util.spec_from_file_location( - path.stem, - path, - ) - - if ( - spec is None - or spec.loader is None - ): - raise RuntimeError( - f"Unable to load config from {path}" - ) - - module = util.module_from_spec( - spec - ) - - spec.loader.exec_module( - module - ) - - return module - - -def load_config_module( - *, - config_path: Path | None = None, - example_path: Path | None = None, -): - - if config_path is None: - - try: - return import_module( - "config" - ) - - except ModuleNotFoundError as error: - - if error.name != "config": - raise - - else: - - if config_path.exists(): - return load_config_from_path( - config_path - ) - - fallback_path = ( - example_path - or ROOT / "config.example.py" - ) - - return load_config_from_path( - fallback_path - ) - - -config = load_config_module() diff --git a/static/images/highlight_v0.1.png b/static/images/highlight_v0.1.png deleted file mode 100644 index 4e163303..00000000 Binary files a/static/images/highlight_v0.1.png and /dev/null differ diff --git a/tools/probe_deep_thought.py b/tests/probe_deep_thought.py similarity index 96% rename from tools/probe_deep_thought.py rename to tests/probe_deep_thought.py index 37862ebd..2333674a 100644 --- a/tools/probe_deep_thought.py +++ b/tests/probe_deep_thought.py @@ -16,14 +16,14 @@ from clients.brain_client import ( build_brain_system_prompt, ) -from contracts.context_contract import ( +from runtime.context_contract import ( DEEP_THOUGHT_ACTION, ) -from settings.app_settings import settings -from settings.config_loader import ( +from app_settings import settings +from config_loader import ( config, ) -from utils.response_extractor import ( +from clients.response_extractor import ( ResponseExtractor, ) from utils.brain import ( diff --git a/tools/run_translation_tests.py b/tests/run_translation_tests.py similarity index 100% rename from tools/run_translation_tests.py rename to tests/run_translation_tests.py diff --git a/tests/test_agent_routing.py b/tests/test_agent_routing.py index cfe424b3..7d9cd9df 100644 --- a/tests/test_agent_routing.py +++ b/tests/test_agent_routing.py @@ -1,14 +1,14 @@ import unittest -from agents.agent_state import ( +from agent.state import ( AgentState, ) -from agents.planner_node import ( +from agent.nodes.planner import ( PlannerNode, ) -from agents.router import ( +from agent.router import ( Router, ) diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index b7a9e52e..d8d2696e 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -4,7 +4,7 @@ build_brain_system_prompt, get_enabled_runtime_actions, ) -from contracts.context_contract import ( +from runtime.context_contract import ( DEEP_THOUGHT_ACTION, SEARCH_ACTION_TEMPLATE, ) diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index 96cb277a..96bd4b66 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -1,7 +1,8 @@ import unittest from pathlib import Path +from unittest.mock import patch -from settings.config_loader import ( +from config_loader import ( load_config_module, ) @@ -31,6 +32,121 @@ def test_falls_back_to_example_config(self): "translator-model", ) + def test_env_overrides_fallback_config_values(self): + + root = Path(__file__).resolve().parents[1] + + with patch.dict( + "os.environ", + { + "BRAIN_MODEL_UID": "env-brain", + "USE_SERVICE_AS_BRAIN": "true", + "BRAIN_CONTEXT_WINDOW": "12345", + "SEARCH_TIMEOUT": "3.5", + }, + clear=True, + ): + config = load_config_module( + config_path=( + root / "missing.config.py" + ), + example_path=( + root / "config.example.py" + ), + ) + + self.assertEqual( + config.BRAIN_MODEL_UID, + "env-brain", + ) + + self.assertIs( + config.USE_SERVICE_AS_BRAIN, + True, + ) + + self.assertEqual( + config.BRAIN_CONTEXT_WINDOW, + 12345, + ) + + self.assertEqual( + config.SEARCH_TIMEOUT, + 3.5, + ) + + def test_prefixed_env_overrides_are_supported(self): + + root = Path(__file__).resolve().parents[1] + + with patch.dict( + "os.environ", + { + "JIN_SERVICE_MODEL_UID": "prefixed-service", + }, + clear=True, + ): + config = load_config_module( + config_path=( + root / "missing.config.py" + ), + example_path=( + root / "config.example.py" + ), + ) + + self.assertEqual( + config.SERVICE_MODEL_UID, + "prefixed-service", + ) + + def test_unprefixed_env_has_priority_over_prefixed_env(self): + + root = Path(__file__).resolve().parents[1] + + with patch.dict( + "os.environ", + { + "SERVICE_MODEL_UID": "plain-service", + "JIN_SERVICE_MODEL_UID": "prefixed-service", + }, + clear=True, + ): + config = load_config_module( + config_path=( + root / "missing.config.py" + ), + example_path=( + root / "config.example.py" + ), + ) + + self.assertEqual( + config.SERVICE_MODEL_UID, + "plain-service", + ) + + def test_invalid_bool_env_value_raises(self): + + root = Path(__file__).resolve().parents[1] + + with patch.dict( + "os.environ", + { + "USE_SERVICE_AS_BRAIN": "maybe", + }, + clear=True, + ): + with self.assertRaises(ValueError): + load_config_module( + config_path=( + root / "missing.config.py" + ), + example_path=( + root / "config.example.py" + ), + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_message_memory.py b/tests/test_message_memory.py index 417a6f77..bcac1fc2 100644 --- a/tests/test_message_memory.py +++ b/tests/test_message_memory.py @@ -4,7 +4,7 @@ from clients.brain_client import ( build_brain_system_prompt, ) -from memory.message_memory import ( +from runtime.memory import ( DEFAULT_RUNTIME_MEMORY, L2_PATCH_WINDOW, build_interrupted_assistant_message, @@ -17,13 +17,13 @@ schedule_runtime_memory_update, summarize_runtime_memory, ) -from runtime.runtime_context import ( +from runtime.context import ( RuntimeContext, ) -from memory.runtime_state import ( +from runtime.state import ( RUNTIME_MEMORY_SUMMARIZER_RUNTIME_ID, ) -from settings.config_loader import ( +from config_loader import ( config, ) diff --git a/tests/test_runtime_actions.py b/tests/test_runtime_actions.py index 2f70cd97..21e4ba07 100644 --- a/tests/test_runtime_actions.py +++ b/tests/test_runtime_actions.py @@ -4,7 +4,7 @@ from clients.brain_client import ( apply_runtime_action_calls, ) -from contracts.context_contract import ( +from runtime.context_contract import ( DEEP_THOUGHT_ACTION, SEARCH_ACTION_CLOSE, SEARCH_ACTION_OPEN, diff --git a/tests/test_runtime_stream_tokens.py b/tests/test_runtime_stream_tokens.py index aabb1394..8dc10174 100644 --- a/tests/test_runtime_stream_tokens.py +++ b/tests/test_runtime_stream_tokens.py @@ -2,13 +2,13 @@ import asyncio from types import SimpleNamespace -from runtime.runtime_registry import ( +from runtime.registry import ( runtime_state, ) -from runtime.runtime_stream import ( +from runtime.stream import ( RuntimeStream, ) -from settings.app_settings import ( +from app_settings import ( settings, ) diff --git a/tests/test_search_flow.py b/tests/test_search_flow.py index ef73b7a0..2b75382e 100644 --- a/tests/test_search_flow.py +++ b/tests/test_search_flow.py @@ -5,10 +5,10 @@ from fastapi import WebSocket -from agents.agent_state import ( +from agent.state import ( AgentState, ) -from agents.brain_node import ( +from agent.nodes.brain import ( BrainNode, ) from clients.search_client import ( @@ -18,18 +18,18 @@ format_search_provider_error, normalize_search_results, ) -from contracts.context_contract import ( +from runtime.context_contract import ( SEARCH_ACTION_CLOSE, SEARCH_ACTION_OPEN, SEARCH_ACTION_TEMPLATE, ) -from emitter.runtime_emitter import ( +from runtime.context import ( RuntimeEmitter, ) -from runtime.runtime_context import ( +from runtime.context import ( RuntimeContext, ) -from utils.search import ( +from clients.search_provider import ( normalize_serper_item, ) from websocket_logger import ( diff --git a/tests/test_translation_node.py b/tests/test_translation_node.py index 222258d2..064b0e31 100644 --- a/tests/test_translation_node.py +++ b/tests/test_translation_node.py @@ -5,15 +5,15 @@ import httpx -from agents.agent_state import ( +from agent.state import ( AgentState, ) -from agents.translation_node import ( +from agent.nodes.translation import ( TranslationNode, ) -from clients.clients_registry import ( +from clients.registry import ( build_clients, ) diff --git a/tests/test_websocket_pending_usage.py b/tests/test_websocket_pending_usage.py index 34ec984b..9b53f269 100644 --- a/tests/test_websocket_pending_usage.py +++ b/tests/test_websocket_pending_usage.py @@ -1,7 +1,7 @@ import unittest from types import SimpleNamespace -from runtime.runtime_registry import ( +from runtime.registry import ( runtime_state, ) from utils.brain import ( diff --git a/static/images/highlight.png b/ui/static/images/highlight.png similarity index 100% rename from static/images/highlight.png rename to ui/static/images/highlight.png diff --git a/static/images/jin-core-history-highlight.png b/ui/static/images/jin-core-history-highlight.png similarity index 100% rename from static/images/jin-core-history-highlight.png rename to ui/static/images/jin-core-history-highlight.png diff --git a/static/images/jin-core-runtime-memory.png b/ui/static/images/jin-core-runtime-memory.png similarity index 100% rename from static/images/jin-core-runtime-memory.png rename to ui/static/images/jin-core-runtime-memory.png diff --git a/static/images/jin-core-split-history.png b/ui/static/images/jin-core-split-history.png similarity index 100% rename from static/images/jin-core-split-history.png rename to ui/static/images/jin-core-split-history.png diff --git a/static/images/states/default.png b/ui/static/images/states/default.png similarity index 100% rename from static/images/states/default.png rename to ui/static/images/states/default.png diff --git a/static/chat.js b/ui/static/js/chat.js similarity index 100% rename from static/chat.js rename to ui/static/js/chat.js diff --git a/static/dragdrop.js b/ui/static/js/dragdrop.js similarity index 100% rename from static/dragdrop.js rename to ui/static/js/dragdrop.js diff --git a/static/logger.js b/ui/static/js/logger.js similarity index 100% rename from static/logger.js rename to ui/static/js/logger.js diff --git a/static/socket.js b/ui/static/js/socket.js similarity index 100% rename from static/socket.js rename to ui/static/js/socket.js diff --git a/static/status.js b/ui/static/js/status.js similarity index 100% rename from static/status.js rename to ui/static/js/status.js diff --git a/static/telemetry.js b/ui/static/js/telemetry.js similarity index 100% rename from static/telemetry.js rename to ui/static/js/telemetry.js diff --git a/templates/index.html b/ui/templates/index.html similarity index 98% rename from templates/index.html rename to ui/templates/index.html index ccc531bc..de65e7a4 100644 --- a/templates/index.html +++ b/ui/templates/index.html @@ -647,12 +647,12 @@