neural-context-protocol 0.1.0a1__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.
- ncp/__init__.py +35 -0
- ncp/adapters/__init__.py +2 -0
- ncp/adapters/anthropic.py +64 -0
- ncp/adapters/base.py +76 -0
- ncp/adapters/cohere.py +48 -0
- ncp/adapters/gemini.py +39 -0
- ncp/adapters/local.py +27 -0
- ncp/adapters/mistral.py +46 -0
- ncp/adapters/ollama.py +53 -0
- ncp/adapters/openai.py +84 -0
- ncp/api.py +210 -0
- ncp/assembler.py +347 -0
- ncp/benchmarks.py +269 -0
- ncp/chunker.py +223 -0
- ncp/cli.py +233 -0
- ncp/coherence.py +101 -0
- ncp/config.py +143 -0
- ncp/costs.py +51 -0
- ncp/dogfood.py +1351 -0
- ncp/encoder.py +121 -0
- ncp/hooks/__init__.py +2 -0
- ncp/mcp/__init__.py +0 -0
- ncp/mcp/server.py +537 -0
- ncp/middleware/__init__.py +13 -0
- ncp/middleware/base.py +88 -0
- ncp/middleware/cost_tracking.py +45 -0
- ncp/middleware/logging.py +67 -0
- ncp/stores/__init__.py +2 -0
- ncp/stores/base.py +96 -0
- ncp/stores/sqlite.py +652 -0
- ncp/templates/config.toml.example +28 -0
- ncp/types.py +423 -0
- ncp/version.py +1 -0
- neural_context_protocol-0.1.0a1.dist-info/METADATA +242 -0
- neural_context_protocol-0.1.0a1.dist-info/RECORD +38 -0
- neural_context_protocol-0.1.0a1.dist-info/WHEEL +5 -0
- neural_context_protocol-0.1.0a1.dist-info/entry_points.txt +2 -0
- neural_context_protocol-0.1.0a1.dist-info/top_level.txt +1 -0
ncp/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Public package surface for Neural Context Protocol."""
|
|
2
|
+
|
|
3
|
+
from .api import agent, configure, emit, get_context, run, stream, write_memory
|
|
4
|
+
from .benchmarks import estimate_tokens, run_coding_pipeline_benchmark, run_research_pipeline_benchmark
|
|
5
|
+
from .dogfood import (
|
|
6
|
+
get_live_provider_readiness,
|
|
7
|
+
load_dogfood_adapter,
|
|
8
|
+
run_adapter_continuation_dogfood_loop,
|
|
9
|
+
run_canonical_dogfood_loop,
|
|
10
|
+
run_canonical_http_dogfood_loop,
|
|
11
|
+
run_live_adapter_continuation_attempt,
|
|
12
|
+
run_repeatability_dogfood_loop,
|
|
13
|
+
)
|
|
14
|
+
from .version import __version__
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"agent",
|
|
19
|
+
"configure",
|
|
20
|
+
"estimate_tokens",
|
|
21
|
+
"emit",
|
|
22
|
+
"get_live_provider_readiness",
|
|
23
|
+
"get_context",
|
|
24
|
+
"load_dogfood_adapter",
|
|
25
|
+
"run_adapter_continuation_dogfood_loop",
|
|
26
|
+
"run_canonical_dogfood_loop",
|
|
27
|
+
"run_canonical_http_dogfood_loop",
|
|
28
|
+
"run_live_adapter_continuation_attempt",
|
|
29
|
+
"run_repeatability_dogfood_loop",
|
|
30
|
+
"run",
|
|
31
|
+
"run_coding_pipeline_benchmark",
|
|
32
|
+
"run_research_pipeline_benchmark",
|
|
33
|
+
"stream",
|
|
34
|
+
"write_memory",
|
|
35
|
+
]
|
ncp/adapters/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from os import environ
|
|
5
|
+
|
|
6
|
+
from ncp.adapters.base import BaseAdapter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AnthropicAdapter(BaseAdapter):
|
|
10
|
+
@property
|
|
11
|
+
def ctx_window(self) -> int:
|
|
12
|
+
return 200000
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str = "",
|
|
17
|
+
model: str = "claude-sonnet-4-20250514",
|
|
18
|
+
max_tokens: int = 4096,
|
|
19
|
+
timeout: float = 120.0,
|
|
20
|
+
) -> None:
|
|
21
|
+
try:
|
|
22
|
+
import anthropic
|
|
23
|
+
except ImportError as err:
|
|
24
|
+
raise ImportError(
|
|
25
|
+
"anthropic is required. Install it with: pip install 'ncp-sdk[providers]'"
|
|
26
|
+
) from err
|
|
27
|
+
self._anthropic = anthropic
|
|
28
|
+
resolved_key = api_key or environ.get("ANTHROPIC_API_KEY", "")
|
|
29
|
+
self._client = anthropic.Anthropic(
|
|
30
|
+
api_key=self._require_api_key(resolved_key, env_var="ANTHROPIC_API_KEY"),
|
|
31
|
+
timeout=timeout,
|
|
32
|
+
)
|
|
33
|
+
self._model = model
|
|
34
|
+
self._max_tokens = max_tokens
|
|
35
|
+
|
|
36
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
37
|
+
msg = self._run_provider_call(
|
|
38
|
+
lambda: self._client.messages.create(
|
|
39
|
+
model=self._model,
|
|
40
|
+
max_tokens=self._max_tokens,
|
|
41
|
+
system=ncp_context,
|
|
42
|
+
messages=[{"role": "user", "content": user_turn}],
|
|
43
|
+
),
|
|
44
|
+
provider="Anthropic",
|
|
45
|
+
timeout_types=(self._anthropic.APITimeoutError, TimeoutError),
|
|
46
|
+
)
|
|
47
|
+
texts = [b.text for b in msg.content if b.type == "text"]
|
|
48
|
+
return self._coerce_text("".join(texts), provider="Anthropic")
|
|
49
|
+
|
|
50
|
+
def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
|
|
51
|
+
stream_ctx = self._run_provider_call(
|
|
52
|
+
lambda: self._client.messages.stream(
|
|
53
|
+
model=self._model,
|
|
54
|
+
max_tokens=self._max_tokens,
|
|
55
|
+
system=ncp_context,
|
|
56
|
+
messages=[{"role": "user", "content": user_turn}],
|
|
57
|
+
),
|
|
58
|
+
provider="Anthropic",
|
|
59
|
+
timeout_types=(self._anthropic.APITimeoutError, TimeoutError),
|
|
60
|
+
)
|
|
61
|
+
with stream_ctx as stream:
|
|
62
|
+
for event in stream:
|
|
63
|
+
if event.type == "content_block_delta" and event.delta.type == "text_delta":
|
|
64
|
+
yield event.delta.text
|
ncp/adapters/base.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Base adapter contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from typing import Callable, TypeVar
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NCPAdapterError(RuntimeError):
|
|
11
|
+
"""Base class for provider adapter failures."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NCPAdapterConfigurationError(NCPAdapterError):
|
|
15
|
+
"""Raised when an adapter is misconfigured before making a call."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NCPAdapterTimeoutError(NCPAdapterError):
|
|
19
|
+
"""Raised when a provider call times out."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NCPAdapterResponseError(NCPAdapterError):
|
|
23
|
+
"""Raised when a provider returns an unusable response."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_T = TypeVar("_T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseAdapter(ABC):
|
|
30
|
+
"""Minimal provider adapter contract for the first NCP API slice."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def ctx_window(self) -> int:
|
|
34
|
+
return 200000
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
38
|
+
"""Return a blocking response for one assembled context."""
|
|
39
|
+
|
|
40
|
+
def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
|
|
41
|
+
"""Yield a streamed response for one assembled context.
|
|
42
|
+
|
|
43
|
+
Tier 2 providers override only if they support streaming.
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError(
|
|
46
|
+
f"{type(self).__name__} does not support streaming in NCP V1; use blocking call()"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _require_api_key(self, api_key: str, *, env_var: str) -> str:
|
|
50
|
+
if api_key.strip():
|
|
51
|
+
return api_key
|
|
52
|
+
raise NCPAdapterConfigurationError(
|
|
53
|
+
f"{type(self).__name__} requires {env_var}; configure it or pass api_key explicitly"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _coerce_text(self, value: str | None, *, provider: str) -> str:
|
|
57
|
+
text = (value or "").strip()
|
|
58
|
+
if text:
|
|
59
|
+
return text
|
|
60
|
+
raise NCPAdapterResponseError(f"{provider} returned an empty text response")
|
|
61
|
+
|
|
62
|
+
def _run_provider_call(
|
|
63
|
+
self,
|
|
64
|
+
call: Callable[[], _T],
|
|
65
|
+
*,
|
|
66
|
+
provider: str,
|
|
67
|
+
timeout_types: tuple[type[BaseException], ...] = (TimeoutError,),
|
|
68
|
+
) -> _T:
|
|
69
|
+
try:
|
|
70
|
+
return call()
|
|
71
|
+
except NCPAdapterError:
|
|
72
|
+
raise
|
|
73
|
+
except timeout_types as exc:
|
|
74
|
+
raise NCPAdapterTimeoutError(f"{provider} timed out: {exc}") from exc
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
raise NCPAdapterError(f"{provider} call failed: {exc}") from exc
|
ncp/adapters/cohere.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from os import environ
|
|
4
|
+
|
|
5
|
+
from ncp.adapters.base import BaseAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CohereAdapter(BaseAdapter):
|
|
9
|
+
@property
|
|
10
|
+
def ctx_window(self) -> int:
|
|
11
|
+
return 128000
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key: str = "",
|
|
16
|
+
model: str = "command-a-03-2025",
|
|
17
|
+
max_tokens: int = 4096,
|
|
18
|
+
timeout: float = 120.0,
|
|
19
|
+
) -> None:
|
|
20
|
+
try:
|
|
21
|
+
import cohere
|
|
22
|
+
except ImportError as err:
|
|
23
|
+
raise ImportError(
|
|
24
|
+
"cohere is required. Install it with: pip install 'ncp-sdk[providers]'"
|
|
25
|
+
) from err
|
|
26
|
+
resolved_key = api_key or environ.get("COHERE_API_KEY", "")
|
|
27
|
+
self._client = cohere.Client(
|
|
28
|
+
api_key=self._require_api_key(resolved_key, env_var="COHERE_API_KEY"),
|
|
29
|
+
timeout=timeout,
|
|
30
|
+
)
|
|
31
|
+
self._model = model
|
|
32
|
+
self._max_tokens = max_tokens
|
|
33
|
+
|
|
34
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
35
|
+
from cohere.types import ChatMessage
|
|
36
|
+
|
|
37
|
+
resp = self._run_provider_call(
|
|
38
|
+
lambda: self._client.chat(
|
|
39
|
+
model=self._model,
|
|
40
|
+
max_tokens=self._max_tokens,
|
|
41
|
+
preamble=ncp_context,
|
|
42
|
+
messages=[
|
|
43
|
+
ChatMessage(role="user", message=user_turn),
|
|
44
|
+
],
|
|
45
|
+
),
|
|
46
|
+
provider="Cohere",
|
|
47
|
+
)
|
|
48
|
+
return self._coerce_text(resp.text, provider="Cohere")
|
ncp/adapters/gemini.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from os import environ
|
|
4
|
+
|
|
5
|
+
from ncp.adapters.base import BaseAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GeminiAdapter(BaseAdapter):
|
|
9
|
+
@property
|
|
10
|
+
def ctx_window(self) -> int:
|
|
11
|
+
return 1000000
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key: str = "",
|
|
16
|
+
model: str = "gemini-2.0-flash",
|
|
17
|
+
timeout: float = 120.0,
|
|
18
|
+
) -> None:
|
|
19
|
+
try:
|
|
20
|
+
import google.generativeai as genai
|
|
21
|
+
except ImportError as err:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"google-generativeai is required. Install it with: pip install 'ncp-sdk[providers]'"
|
|
24
|
+
) from err
|
|
25
|
+
resolved_key = api_key or environ.get("GOOGLE_API_KEY", "")
|
|
26
|
+
genai.configure(api_key=self._require_api_key(resolved_key, env_var="GOOGLE_API_KEY"))
|
|
27
|
+
self._model = genai.GenerativeModel(model)
|
|
28
|
+
self._timeout = timeout
|
|
29
|
+
|
|
30
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
31
|
+
prompt = f"{ncp_context}\n\n{user_turn}"
|
|
32
|
+
resp = self._run_provider_call(
|
|
33
|
+
lambda: self._model.generate_content(
|
|
34
|
+
prompt,
|
|
35
|
+
request_options={"timeout": self._timeout},
|
|
36
|
+
),
|
|
37
|
+
provider="Gemini",
|
|
38
|
+
)
|
|
39
|
+
return self._coerce_text(resp.text, provider="Gemini")
|
ncp/adapters/local.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Local deterministic adapter for early dogfood and testing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
|
|
7
|
+
from ncp.adapters.base import BaseAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LocalAdapter(BaseAdapter):
|
|
11
|
+
"""A simple blocking/streaming adapter for the SQLite-first local path."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def ctx_window(self) -> int:
|
|
15
|
+
return 200000
|
|
16
|
+
|
|
17
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
18
|
+
return (
|
|
19
|
+
"local_adapter_response\n"
|
|
20
|
+
f"user_turn:{user_turn}\n"
|
|
21
|
+
f"context_chars:{len(ncp_context)}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
|
|
25
|
+
response = self.call(ncp_context, user_turn)
|
|
26
|
+
for line in response.splitlines(keepends=True):
|
|
27
|
+
yield line
|
ncp/adapters/mistral.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from os import environ
|
|
4
|
+
|
|
5
|
+
from ncp.adapters.base import BaseAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MistralAdapter(BaseAdapter):
|
|
9
|
+
@property
|
|
10
|
+
def ctx_window(self) -> int:
|
|
11
|
+
return 128000
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key: str = "",
|
|
16
|
+
model: str = "mistral-large-latest",
|
|
17
|
+
max_tokens: int = 4096,
|
|
18
|
+
timeout: float = 120.0,
|
|
19
|
+
) -> None:
|
|
20
|
+
try:
|
|
21
|
+
from mistralai.client import Mistral
|
|
22
|
+
except ImportError as err:
|
|
23
|
+
raise ImportError(
|
|
24
|
+
"mistralai is required. Install it with: pip install 'ncp-sdk[providers]'"
|
|
25
|
+
) from err
|
|
26
|
+
resolved_key = api_key or environ.get("MISTRAL_API_KEY", "")
|
|
27
|
+
self._client = Mistral(
|
|
28
|
+
api_key=self._require_api_key(resolved_key, env_var="MISTRAL_API_KEY"),
|
|
29
|
+
timeout_ms=int(timeout * 1000),
|
|
30
|
+
)
|
|
31
|
+
self._model = model
|
|
32
|
+
self._max_tokens = max_tokens
|
|
33
|
+
|
|
34
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
35
|
+
resp = self._run_provider_call(
|
|
36
|
+
lambda: self._client.chat.complete(
|
|
37
|
+
model=self._model,
|
|
38
|
+
max_tokens=self._max_tokens,
|
|
39
|
+
messages=[
|
|
40
|
+
{"role": "system", "content": ncp_context},
|
|
41
|
+
{"role": "user", "content": user_turn},
|
|
42
|
+
],
|
|
43
|
+
),
|
|
44
|
+
provider="Mistral",
|
|
45
|
+
)
|
|
46
|
+
return self._coerce_text(resp.choices[0].message.content, provider="Mistral")
|
ncp/adapters/ollama.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from os import environ
|
|
4
|
+
|
|
5
|
+
from ncp.adapters.base import BaseAdapter
|
|
6
|
+
|
|
7
|
+
_DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434/v1"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OllamaAdapter(BaseAdapter):
|
|
11
|
+
@property
|
|
12
|
+
def ctx_window(self) -> int:
|
|
13
|
+
return 8192
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
base_url: str = "",
|
|
18
|
+
model: str = "llama3.1",
|
|
19
|
+
max_tokens: int = 4096,
|
|
20
|
+
timeout: float = 120.0,
|
|
21
|
+
max_retries: int = 2,
|
|
22
|
+
) -> None:
|
|
23
|
+
try:
|
|
24
|
+
import openai
|
|
25
|
+
except ImportError as err:
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"openai is required. Install it with: pip install 'ncp-sdk[providers]'"
|
|
28
|
+
) from err
|
|
29
|
+
self._openai = openai
|
|
30
|
+
resolved_base_url = base_url or environ.get("OLLAMA_BASE_URL", _DEFAULT_OLLAMA_BASE_URL)
|
|
31
|
+
self._client = openai.OpenAI(
|
|
32
|
+
base_url=resolved_base_url,
|
|
33
|
+
api_key="ollama",
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
max_retries=max_retries,
|
|
36
|
+
)
|
|
37
|
+
self._model = model
|
|
38
|
+
self._max_tokens = max_tokens
|
|
39
|
+
|
|
40
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
41
|
+
resp = self._run_provider_call(
|
|
42
|
+
lambda: self._client.chat.completions.create(
|
|
43
|
+
model=self._model,
|
|
44
|
+
max_tokens=self._max_tokens,
|
|
45
|
+
messages=[
|
|
46
|
+
{"role": "system", "content": ncp_context},
|
|
47
|
+
{"role": "user", "content": user_turn},
|
|
48
|
+
],
|
|
49
|
+
),
|
|
50
|
+
provider="Ollama",
|
|
51
|
+
timeout_types=(self._openai.APITimeoutError, TimeoutError),
|
|
52
|
+
)
|
|
53
|
+
return self._coerce_text(resp.choices[0].message.content, provider="Ollama")
|
ncp/adapters/openai.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from os import environ
|
|
5
|
+
|
|
6
|
+
from ncp.adapters.base import BaseAdapter
|
|
7
|
+
|
|
8
|
+
_MODEL_WINDOWS: dict[str, int] = {
|
|
9
|
+
"gpt-4o": 128000,
|
|
10
|
+
"gpt-4o-mini": 128000,
|
|
11
|
+
"o1": 200000,
|
|
12
|
+
"o3-mini": 200000,
|
|
13
|
+
"gpt-4.1": 1047576,
|
|
14
|
+
"gpt-4.1-mini": 1047576,
|
|
15
|
+
"gpt-4.1-nano": 1047576,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OpenAIAdapter(BaseAdapter):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
api_key: str = "",
|
|
23
|
+
model: str = "gpt-4o",
|
|
24
|
+
max_tokens: int = 4096,
|
|
25
|
+
timeout: float = 120.0,
|
|
26
|
+
max_retries: int = 2,
|
|
27
|
+
base_url: str | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
try:
|
|
30
|
+
import openai
|
|
31
|
+
except ImportError as err:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"openai is required. Install it with: pip install 'ncp-sdk[providers]'"
|
|
34
|
+
) from err
|
|
35
|
+
self._openai = openai
|
|
36
|
+
resolved_key = api_key or environ.get("OPENAI_API_KEY", "")
|
|
37
|
+
kwargs: dict = {
|
|
38
|
+
"api_key": self._require_api_key(resolved_key, env_var="OPENAI_API_KEY"),
|
|
39
|
+
"timeout": timeout,
|
|
40
|
+
"max_retries": max_retries,
|
|
41
|
+
}
|
|
42
|
+
if base_url is not None:
|
|
43
|
+
kwargs["base_url"] = base_url
|
|
44
|
+
self._client = openai.OpenAI(**kwargs)
|
|
45
|
+
self._model = model
|
|
46
|
+
self._max_tokens = max_tokens
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def ctx_window(self) -> int:
|
|
50
|
+
return _MODEL_WINDOWS.get(self._model, 128000)
|
|
51
|
+
|
|
52
|
+
def call(self, ncp_context: str, user_turn: str) -> str:
|
|
53
|
+
resp = self._run_provider_call(
|
|
54
|
+
lambda: self._client.chat.completions.create(
|
|
55
|
+
model=self._model,
|
|
56
|
+
max_tokens=self._max_tokens,
|
|
57
|
+
messages=[
|
|
58
|
+
{"role": "system", "content": ncp_context},
|
|
59
|
+
{"role": "user", "content": user_turn},
|
|
60
|
+
],
|
|
61
|
+
stream=False,
|
|
62
|
+
),
|
|
63
|
+
provider="OpenAI",
|
|
64
|
+
timeout_types=(self._openai.APITimeoutError, TimeoutError),
|
|
65
|
+
)
|
|
66
|
+
return self._coerce_text(resp.choices[0].message.content, provider="OpenAI")
|
|
67
|
+
|
|
68
|
+
def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
|
|
69
|
+
stream = self._run_provider_call(
|
|
70
|
+
lambda: self._client.chat.completions.create(
|
|
71
|
+
model=self._model,
|
|
72
|
+
max_tokens=self._max_tokens,
|
|
73
|
+
messages=[
|
|
74
|
+
{"role": "system", "content": ncp_context},
|
|
75
|
+
{"role": "user", "content": user_turn},
|
|
76
|
+
],
|
|
77
|
+
stream=True,
|
|
78
|
+
),
|
|
79
|
+
provider="OpenAI",
|
|
80
|
+
timeout_types=(self._openai.APITimeoutError, TimeoutError),
|
|
81
|
+
)
|
|
82
|
+
for chunk in stream:
|
|
83
|
+
if chunk.choices and (delta := chunk.choices[0].delta.content):
|
|
84
|
+
yield delta
|
ncp/api.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Public API helpers for the SQLite-first NCP runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from ncp.adapters.base import BaseAdapter
|
|
9
|
+
from ncp.adapters.local import LocalAdapter
|
|
10
|
+
from ncp.assembler import Assembler
|
|
11
|
+
from ncp.config import NCPConfig, load_config
|
|
12
|
+
from ncp.stores.sqlite import SQLiteStore
|
|
13
|
+
from ncp.types import BudgetContext, ConsciousBlock, NCPResponse, SubconsciousChunk, Whisper
|
|
14
|
+
|
|
15
|
+
_CONFIG: NCPConfig | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def configure(
|
|
19
|
+
*,
|
|
20
|
+
path: str | Path | None = None,
|
|
21
|
+
cwd: str | Path | None = None,
|
|
22
|
+
env: dict[str, str] | None = None,
|
|
23
|
+
) -> NCPConfig:
|
|
24
|
+
"""Load and cache the active NCP configuration."""
|
|
25
|
+
|
|
26
|
+
global _CONFIG
|
|
27
|
+
_CONFIG = load_config(path=path, cwd=cwd, env=env)
|
|
28
|
+
return _CONFIG
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def agent(
|
|
32
|
+
*,
|
|
33
|
+
id: str,
|
|
34
|
+
role: str,
|
|
35
|
+
owns: list[str] | None = None,
|
|
36
|
+
must_not: list[str] | None = None,
|
|
37
|
+
task: str = "idle",
|
|
38
|
+
slot: str = "unassigned",
|
|
39
|
+
intent: str = "maintain_context",
|
|
40
|
+
**overrides: object,
|
|
41
|
+
) -> ConsciousBlock:
|
|
42
|
+
"""Create a conscious-block template through the public API."""
|
|
43
|
+
|
|
44
|
+
payload = {
|
|
45
|
+
"agent_id": id,
|
|
46
|
+
"role": role,
|
|
47
|
+
"owns": owns or [],
|
|
48
|
+
"must_not": must_not or [],
|
|
49
|
+
"task": task,
|
|
50
|
+
"slot": slot,
|
|
51
|
+
"intent": intent,
|
|
52
|
+
**overrides,
|
|
53
|
+
}
|
|
54
|
+
return ConsciousBlock(**payload)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_context(
|
|
58
|
+
*,
|
|
59
|
+
agent: ConsciousBlock,
|
|
60
|
+
budget: BudgetContext | None = None,
|
|
61
|
+
query_text: str | None = None,
|
|
62
|
+
store: SQLiteStore | None = None,
|
|
63
|
+
config: NCPConfig | None = None,
|
|
64
|
+
) -> str:
|
|
65
|
+
"""Assemble raw pidgin context for one agent turn."""
|
|
66
|
+
|
|
67
|
+
resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
|
|
68
|
+
resolved_store = store or SQLiteStore(resolved_config.store_path)
|
|
69
|
+
assembler = Assembler(store=resolved_store)
|
|
70
|
+
return assembler.assemble(
|
|
71
|
+
conscious=agent,
|
|
72
|
+
budget=budget or BudgetContext(),
|
|
73
|
+
query_text=query_text,
|
|
74
|
+
).context
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def write_memory(
|
|
78
|
+
chunk: SubconsciousChunk,
|
|
79
|
+
*,
|
|
80
|
+
store: SQLiteStore | None = None,
|
|
81
|
+
config: NCPConfig | None = None,
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""Persist one chunk through the public API."""
|
|
84
|
+
|
|
85
|
+
resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
|
|
86
|
+
resolved_store = store or SQLiteStore(resolved_config.store_path)
|
|
87
|
+
return resolved_store.write(chunk)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def emit(
|
|
91
|
+
whisper: Whisper,
|
|
92
|
+
*,
|
|
93
|
+
store: SQLiteStore | None = None,
|
|
94
|
+
config: NCPConfig | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Persist one whisper through the public API."""
|
|
97
|
+
|
|
98
|
+
resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
|
|
99
|
+
resolved_store = store or SQLiteStore(resolved_config.store_path)
|
|
100
|
+
resolved_store.emit_whisper(whisper)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run(
|
|
104
|
+
*,
|
|
105
|
+
agent: ConsciousBlock,
|
|
106
|
+
turn: str,
|
|
107
|
+
adapter: BaseAdapter | None = None,
|
|
108
|
+
budget: BudgetContext | None = None,
|
|
109
|
+
query_text: str | None = None,
|
|
110
|
+
store: SQLiteStore | None = None,
|
|
111
|
+
config: NCPConfig | None = None,
|
|
112
|
+
) -> NCPResponse:
|
|
113
|
+
"""Run one blocking local-runtime call through an adapter."""
|
|
114
|
+
|
|
115
|
+
resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
|
|
116
|
+
resolved_store = store or SQLiteStore(resolved_config.store_path)
|
|
117
|
+
resolved_budget = budget or BudgetContext()
|
|
118
|
+
resolved_adapter = adapter or LocalAdapter()
|
|
119
|
+
assembler = Assembler(store=resolved_store)
|
|
120
|
+
|
|
121
|
+
start = time.perf_counter()
|
|
122
|
+
assembly = assembler.assemble(
|
|
123
|
+
conscious=agent,
|
|
124
|
+
budget=resolved_budget,
|
|
125
|
+
query_text=query_text or turn,
|
|
126
|
+
ctx_window=resolved_adapter.ctx_window,
|
|
127
|
+
)
|
|
128
|
+
content = resolved_adapter.call(assembly.context, turn)
|
|
129
|
+
response = _build_response(
|
|
130
|
+
agent=agent,
|
|
131
|
+
adapter=resolved_adapter,
|
|
132
|
+
context=assembly.context,
|
|
133
|
+
turn=turn,
|
|
134
|
+
content=content,
|
|
135
|
+
start=start,
|
|
136
|
+
)
|
|
137
|
+
assembler.post_turn(
|
|
138
|
+
conscious=agent,
|
|
139
|
+
response=response,
|
|
140
|
+
result_summary=content.splitlines()[0] if content else "",
|
|
141
|
+
result_full=content,
|
|
142
|
+
)
|
|
143
|
+
return response
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def stream(
|
|
147
|
+
*,
|
|
148
|
+
agent: ConsciousBlock,
|
|
149
|
+
turn: str,
|
|
150
|
+
adapter: BaseAdapter | None = None,
|
|
151
|
+
budget: BudgetContext | None = None,
|
|
152
|
+
query_text: str | None = None,
|
|
153
|
+
store: SQLiteStore | None = None,
|
|
154
|
+
config: NCPConfig | None = None,
|
|
155
|
+
):
|
|
156
|
+
"""Yield a streamed response through the adapter."""
|
|
157
|
+
|
|
158
|
+
resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
|
|
159
|
+
resolved_store = store or SQLiteStore(resolved_config.store_path)
|
|
160
|
+
resolved_budget = budget or BudgetContext()
|
|
161
|
+
resolved_adapter = adapter or LocalAdapter()
|
|
162
|
+
assembler = Assembler(store=resolved_store)
|
|
163
|
+
assembly = assembler.assemble(
|
|
164
|
+
conscious=agent,
|
|
165
|
+
budget=resolved_budget,
|
|
166
|
+
query_text=query_text or turn,
|
|
167
|
+
ctx_window=resolved_adapter.ctx_window,
|
|
168
|
+
)
|
|
169
|
+
start = time.perf_counter()
|
|
170
|
+
chunks: list[str] = []
|
|
171
|
+
for chunk in resolved_adapter.stream(assembly.context, turn):
|
|
172
|
+
chunks.append(chunk)
|
|
173
|
+
yield chunk
|
|
174
|
+
|
|
175
|
+
content = "".join(chunks)
|
|
176
|
+
response = _build_response(
|
|
177
|
+
agent=agent,
|
|
178
|
+
adapter=resolved_adapter,
|
|
179
|
+
context=assembly.context,
|
|
180
|
+
turn=turn,
|
|
181
|
+
content=content,
|
|
182
|
+
start=start,
|
|
183
|
+
)
|
|
184
|
+
assembler.post_turn(
|
|
185
|
+
conscious=agent,
|
|
186
|
+
response=response,
|
|
187
|
+
result_summary=content.splitlines()[0] if content else "",
|
|
188
|
+
result_full=content,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _build_response(
|
|
193
|
+
*,
|
|
194
|
+
agent: ConsciousBlock,
|
|
195
|
+
adapter: BaseAdapter,
|
|
196
|
+
context: str,
|
|
197
|
+
turn: str,
|
|
198
|
+
content: str,
|
|
199
|
+
start: float,
|
|
200
|
+
) -> NCPResponse:
|
|
201
|
+
return NCPResponse(
|
|
202
|
+
content=content,
|
|
203
|
+
input_tokens=len((context + "\n" + turn).split()),
|
|
204
|
+
output_tokens=len(content.split()),
|
|
205
|
+
cost_usd=0.0,
|
|
206
|
+
model=adapter.__class__.__name__.lower(),
|
|
207
|
+
pipeline_id=agent.pipeline_id,
|
|
208
|
+
turn_id=f"turn_{int(time.time() * 1000)}",
|
|
209
|
+
latency_ms=int((time.perf_counter() - start) * 1000),
|
|
210
|
+
)
|