rpr-cli 0.3.7__cp314-abi3-win_amd64.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.
- rpr/__init__.py +1 -0
- rpr/_native.pyd +0 -0
- rpr/_native.pyi +32 -0
- rpr/agent/__init__.py +29 -0
- rpr/agent/approval.py +19 -0
- rpr/agent/bootstrap.py +60 -0
- rpr/agent/client.py +115 -0
- rpr/agent/control.py +16 -0
- rpr/agent/mock.py +158 -0
- rpr/agent/runtime.py +291 -0
- rpr/agent/session.py +91 -0
- rpr/agent/tools/__init__.py +10 -0
- rpr/agent/tools/base.py +62 -0
- rpr/agent/tools/mutating.py +47 -0
- rpr/agent/tools/readonly.py +147 -0
- rpr/agent/tools/registry.py +30 -0
- rpr/application/__init__.py +1 -0
- rpr/application/catalog.py +485 -0
- rpr/application/chat_service.py +413 -0
- rpr/application/checks.py +49 -0
- rpr/application/cli_adapter.py +36 -0
- rpr/application/completer.py +163 -0
- rpr/application/conversation_service.py +92 -0
- rpr/application/prompt_service.py +85 -0
- rpr/application/selector.py +240 -0
- rpr/application/shell.py +541 -0
- rpr/checks/__init__.py +0 -0
- rpr/checks/base.py +13 -0
- rpr/checks/instructions.py +142 -0
- rpr/checks/packages.py +619 -0
- rpr/checks/workspace.py +176 -0
- rpr/cli.py +94 -0
- rpr/commands/__init__.py +0 -0
- rpr/commands/add.py +624 -0
- rpr/commands/chat.py +48 -0
- rpr/commands/check.py +53 -0
- rpr/commands/generate/__init__.py +0 -0
- rpr/commands/generate/api.py +249 -0
- rpr/commands/generate/domain.py +434 -0
- rpr/commands/generate/engine.py +156 -0
- rpr/commands/generate/storybook.py +412 -0
- rpr/commands/generate/ui.py +481 -0
- rpr/commands/init.py +951 -0
- rpr/commands/map.py +113 -0
- rpr/commands/refresh.py +65 -0
- rpr/commands/rename.py +104 -0
- rpr/commands/settings.py +102 -0
- rpr/commands/sync.py +121 -0
- rpr/commands/update.py +222 -0
- rpr/context.py +275 -0
- rpr/dependencies.py +212 -0
- rpr/dependency_manifest_data.py +270 -0
- rpr/generators/__init__.py +0 -0
- rpr/generators/base.py +134 -0
- rpr/generators/claude.py +57 -0
- rpr/generators/codex.py +46 -0
- rpr/generators/copilot.py +31 -0
- rpr/generators/cursor.py +38 -0
- rpr/generators/gemini.py +33 -0
- rpr/generators/github.py +35 -0
- rpr/manifest_refresh.py +212 -0
- rpr/map/__init__.py +0 -0
- rpr/map/architecture.py +495 -0
- rpr/map/chains.py +317 -0
- rpr/map/classifier.py +170 -0
- rpr/map/coverage.py +200 -0
- rpr/map/dependencies.py +243 -0
- rpr/map/extractor.py +223 -0
- rpr/map/graph.py +318 -0
- rpr/map/output.py +1030 -0
- rpr/map/responsibility.py +345 -0
- rpr/map/topology.py +327 -0
- rpr/map/walker.py +151 -0
- rpr/package_updates.py +731 -0
- rpr/project_rename.py +129 -0
- rpr/scaffolds/domain/base_entity.md +30 -0
- rpr/scaffolds/domain/base_repo.md +48 -0
- rpr/scaffolds/domain/container.md +74 -0
- rpr/scaffolds/domain/settings.md +91 -0
- rpr/scaffolds/instructions/all.instructions.md +49 -0
- rpr/scaffolds/instructions/api.instructions.md +42 -0
- rpr/scaffolds/instructions/domain.instructions.md +96 -0
- rpr/scaffolds/instructions/frontend.instructions.md +110 -0
- rpr/scaffolds/instructions/rust-engine.instructions.md +41 -0
- rpr/scaffolds/instructions/setup-guide.instructions.md +89 -0
- rpr/scaffolds/instructions/tooling-setup.instructions.md +91 -0
- rpr/scaffolds/instructions/tooling.instructions.md +43 -0
- rpr/scaffolds/js_special_files/chat/BACKEND_FLOW.md +321 -0
- rpr/scaffolds/js_special_files/chat/README.md +623 -0
- rpr/scaffolds/js_special_files/chat/client/content.ts +47 -0
- rpr/scaffolds/js_special_files/chat/client/httpChatStreamClient.spec.ts +227 -0
- rpr/scaffolds/js_special_files/chat/client/httpChatStreamClient.ts +201 -0
- rpr/scaffolds/js_special_files/chat/client/index.ts +2 -0
- rpr/scaffolds/js_special_files/chat/client/types.ts +74 -0
- rpr/scaffolds/js_special_files/chat/composer/ChatComposer.spec.tsx +363 -0
- rpr/scaffolds/js_special_files/chat/composer/ChatComposer.tsx +444 -0
- rpr/scaffolds/js_special_files/chat/composer/__screenshots__/ChatComposer.spec.tsx/ChatComposer-renders-custom-action-buttons-beside-the-attachment-control-1.png +0 -0
- rpr/scaffolds/js_special_files/chat/composer/fileDrop.spec.ts +60 -0
- rpr/scaffolds/js_special_files/chat/composer/fileDrop.ts +128 -0
- rpr/scaffolds/js_special_files/chat/composer/index.ts +6 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/ChatComposerMentionTokens.tsx +128 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/ChatSuggestionsPopover.tsx +125 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/LocalFileSuggestionAction.tsx +100 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/index.ts +7 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/localFileSuggestions.spec.ts +282 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/localFileSuggestions.ts +1004 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/triggerDetection.spec.ts +56 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/triggerDetection.ts +58 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/types.ts +124 -0
- rpr/scaffolds/js_special_files/chat/composer/suggestions/useChatSuggestions.ts +330 -0
- rpr/scaffolds/js_special_files/chat/composer/types.ts +166 -0
- rpr/scaffolds/js_special_files/chat/composer/useChatComposer.ts +249 -0
- rpr/scaffolds/js_special_files/chat/composer/useChatStream.spec.tsx +312 -0
- rpr/scaffolds/js_special_files/chat/composer/useChatStream.ts +527 -0
- rpr/scaffolds/js_special_files/chat/experience/ChatExperience.spec.tsx +142 -0
- rpr/scaffolds/js_special_files/chat/experience/ChatExperience.styles.tsx +101 -0
- rpr/scaffolds/js_special_files/chat/experience/ChatExperience.tsx +166 -0
- rpr/scaffolds/js_special_files/chat/experience/ChatExperienceComposer.styles.tsx +101 -0
- rpr/scaffolds/js_special_files/chat/experience/ChatExperienceComposer.tsx +176 -0
- rpr/scaffolds/js_special_files/chat/experience/ChatExperienceHeader.styles.tsx +29 -0
- rpr/scaffolds/js_special_files/chat/experience/ChatExperienceHeader.tsx +45 -0
- rpr/scaffolds/js_special_files/chat/experience/ThreadHistory.styles.tsx +80 -0
- rpr/scaffolds/js_special_files/chat/experience/ThreadHistory.tsx +127 -0
- rpr/scaffolds/js_special_files/chat/experience/chatStorage.spec.ts +98 -0
- rpr/scaffolds/js_special_files/chat/experience/chatStorage.ts +128 -0
- rpr/scaffolds/js_special_files/chat/experience/index.ts +7 -0
- rpr/scaffolds/js_special_files/chat/experience/types.ts +108 -0
- rpr/scaffolds/js_special_files/chat/experience/useChatExperience.spec.tsx +148 -0
- rpr/scaffolds/js_special_files/chat/experience/useChatExperience.ts +473 -0
- rpr/scaffolds/js_special_files/chat/index.ts +5 -0
- rpr/scaffolds/js_special_files/chat/testing/createMockChatStreamClient.ts +153 -0
- rpr/scaffolds/js_special_files/chat/testing/createMockSuggestionProvider.ts +103 -0
- rpr/scaffolds/js_special_files/chat/testing/index.ts +2 -0
- rpr/scaffolds/js_special_files/chat/thread/ChatMessage.tsx +268 -0
- rpr/scaffolds/js_special_files/chat/thread/ChatMessageActions.tsx +139 -0
- rpr/scaffolds/js_special_files/chat/thread/ChatMessageContent.spec.tsx +163 -0
- rpr/scaffolds/js_special_files/chat/thread/ChatMessageContent.tsx +690 -0
- rpr/scaffolds/js_special_files/chat/thread/ChatThread.spec.tsx +365 -0
- rpr/scaffolds/js_special_files/chat/thread/ChatThread.tsx +172 -0
- rpr/scaffolds/js_special_files/chat/thread/README.md +111 -0
- rpr/scaffolds/js_special_files/chat/thread/contentFormat.ts +30 -0
- rpr/scaffolds/js_special_files/chat/thread/index.ts +7 -0
- rpr/scaffolds/js_special_files/chat/thread/streaming.ts +176 -0
- rpr/scaffolds/js_special_files/chat/thread/types.ts +189 -0
- rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponent.spec.tsx +147 -0
- rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponent.tsx +94 -0
- rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponentList.spec.tsx +277 -0
- rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponentList.tsx +121 -0
- rpr/scaffolds/js_special_files/chat/virtual-list/index.ts +1 -0
- rpr/scaffolds/js_special_files/fetch.service.md +222 -0
- rpr/scaffolds/js_special_files/sticky-navigation.md +170 -0
- rpr/scaffolds/js_special_files/sticky-navigation.spec.md +231 -0
- rpr/scaffolds/js_special_files/vitest-browser-config.md +24 -0
- rpr/scaffolds/py_special_files/chat/api/routes/chat_routes.py +242 -0
- rpr/scaffolds/py_special_files/chat/api/tests/test_chat.py +287 -0
- rpr/scaffolds/py_special_files/chat/domain/models/dtos/chat_dtos.py +450 -0
- rpr/scaffolds/py_special_files/chat/domain/models/dtos/conversation_history_dtos.py +37 -0
- rpr/scaffolds/py_special_files/chat/domain/models/entities/conversation_history.py +122 -0
- rpr/scaffolds/py_special_files/chat/domain/repos/conversation_history.py +349 -0
- rpr/scaffolds/py_special_files/chat/domain/services/agent_session_codec.py +53 -0
- rpr/scaffolds/py_special_files/chat/domain/services/chat_service.py +222 -0
- rpr/scaffolds/py_special_files/chat/domain/services/conversation_history_service.py +192 -0
- rpr/scaffolds/py_special_files/chat/domain/services/conversation_operations.py +148 -0
- rpr/scaffolds/py_special_files/chat/domain/services/history_codec.py +146 -0
- rpr/scaffolds/py_special_files/chat/domain/services/in_memory_history_store.py +94 -0
- rpr/scaffolds/py_special_files/chat/domain/services/postgres_history_store.py +143 -0
- rpr/scaffolds/py_special_files/chat/domain/services/protocols/conversation_history_store.py +56 -0
- rpr/scaffolds/py_special_files/chat/domain/services/protocols/conversation_runtime.py +90 -0
- rpr/scaffolds/py_special_files/chat/domain/services/protocols/llm_provider.py +52 -0
- rpr/scaffolds/py_special_files/chat/domain/tests/test_chat_dtos.py +110 -0
- rpr/scaffolds/py_special_files/chat/domain/tests/test_chat_service.py +401 -0
- rpr/scaffolds/py_special_files/chat/domain/tests/test_history_codec.py +122 -0
- rpr/scaffolds/py_special_files/chat/domain/tests/test_history_projection.py +19 -0
- rpr/scaffolds/py_special_files/chat/domain/tests/test_persistence_config.py +24 -0
- rpr/scaffolds/py_special_files/chat/domain/tests/test_postgres_history_store.py +165 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/models/dtos/__init__.py +3 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/models/dtos/vllm_model_dtos.py +113 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/VLLM_SETUP.md +31 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/__init__.py +18 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/model_catalog.py +84 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/vllm_agent_factory.py +129 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/vllm_agent_factory_protocol.py +18 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/vllm_inference_coordinator.py +207 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/runtimes/__init__.py +3 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/runtimes/agents_sdk.py +211 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/services/runtimes/agents_sdk_message_mapping.py +120 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/tests/test_agents_sdk_runtime.py +205 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/tests/test_vllm_model_catalog.py +768 -0
- rpr/scaffolds/py_special_files/vllm_provider/domain/tests/test_vllm_provider.py +110 -0
- rpr/scaffolds/special_files/camel_case_model.md +60 -0
- rpr/scaffolds/special_files/domain_container.md +120 -0
- rpr/scaffolds/special_files/domain_settings.md +74 -0
- rpr/scaffolds/special_files/dto_util.md +62 -0
- rpr/scaffolds/special_files/encrypted_column.md +119 -0
- rpr/scaffolds/special_files/logging.md +32 -0
- rpr/scaffolds/special_files/mapper_util.md +159 -0
- rpr/scaffolds/special_files/partial_update.md +61 -0
- rpr/scaffolds/special_files/test_encrypted_column.md +103 -0
- rpr/templates/__init__.py +0 -0
- rpr/templates/registry.py +414 -0
- rpr/ui/__init__.py +0 -0
- rpr/ui/console.py +32 -0
- rpr/ui/markdown.py +59 -0
- rpr/ui/prompt_session.py +430 -0
- rpr/ui/renderers.py +167 -0
- rpr/ui/theme.py +286 -0
- rpr/workspace.py +139 -0
- rpr_cli-0.3.7.dist-info/METADATA +283 -0
- rpr_cli-0.3.7.dist-info/RECORD +213 -0
- rpr_cli-0.3.7.dist-info/WHEEL +4 -0
- rpr_cli-0.3.7.dist-info/entry_points.txt +2 -0
- rpr_cli-0.3.7.dist-info/licenses/LICENSE +21 -0
- rpr_cli-0.3.7.dist-info/sboms/rpr-parser.cyclonedx.json +11447 -0
rpr/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.7"
|
rpr/_native.pyd
ADDED
|
Binary file
|
rpr/_native.pyi
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Literal, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Language = Literal["python", "typescript", "javascript", "rust", "cpp"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DependencyNode(TypedDict):
|
|
8
|
+
abs_path: str
|
|
9
|
+
rel_path: str
|
|
10
|
+
language: Language
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DependencyGraphPayload(TypedDict):
|
|
14
|
+
nodes: list[DependencyNode]
|
|
15
|
+
edges: dict[str, list[str]]
|
|
16
|
+
reverse_edges: dict[str, list[str]]
|
|
17
|
+
cycles: list[list[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RenamePlan:
|
|
21
|
+
@property
|
|
22
|
+
def backend(self) -> Literal["rust"]: ...
|
|
23
|
+
def summary(self) -> dict[str, object]: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def init_env() -> None: ...
|
|
27
|
+
def close_env() -> None: ...
|
|
28
|
+
def reset_logging() -> None: ...
|
|
29
|
+
def build_dependency_graph(abs_root: str) -> DependencyGraphPayload: ...
|
|
30
|
+
def ensure_rename_ignore(abs_root: str) -> bool: ...
|
|
31
|
+
def plan_project_rename(abs_root: str, new_name: str) -> RenamePlan: ...
|
|
32
|
+
def apply_project_rename(plan: RenamePlan) -> None: ...
|
rpr/agent/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from rpr.agent.approval import ApprovalDecision, ApprovalMode
|
|
2
|
+
from rpr.agent.client import (
|
|
3
|
+
AgentClient,
|
|
4
|
+
AgentEvent,
|
|
5
|
+
ConversationMessage,
|
|
6
|
+
TextDeltaEvent,
|
|
7
|
+
ToolCallRequestEvent,
|
|
8
|
+
TurnCompleteEvent,
|
|
9
|
+
)
|
|
10
|
+
from rpr.agent.control import TurnControl
|
|
11
|
+
from rpr.agent.runtime import AgentRuntime, RuntimeCallbacks
|
|
12
|
+
from rpr.agent.session import ChatSession, ToolCallRecord, ToolCallStatus
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgentClient",
|
|
16
|
+
"AgentEvent",
|
|
17
|
+
"AgentRuntime",
|
|
18
|
+
"ApprovalDecision",
|
|
19
|
+
"ApprovalMode",
|
|
20
|
+
"ChatSession",
|
|
21
|
+
"ConversationMessage",
|
|
22
|
+
"RuntimeCallbacks",
|
|
23
|
+
"TextDeltaEvent",
|
|
24
|
+
"TurnControl",
|
|
25
|
+
"ToolCallRecord",
|
|
26
|
+
"ToolCallRequestEvent",
|
|
27
|
+
"ToolCallStatus",
|
|
28
|
+
"TurnCompleteEvent",
|
|
29
|
+
]
|
rpr/agent/approval.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApprovalMode(StrEnum):
|
|
7
|
+
ASK = "ask"
|
|
8
|
+
AUTO_READONLY = "auto-readonly"
|
|
9
|
+
AUTO_ALL = "auto-all"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApprovalDecision(StrEnum):
|
|
13
|
+
APPROVE = "approve"
|
|
14
|
+
REJECT = "reject"
|
|
15
|
+
ALWAYS_ALLOW = "always-allow"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def requires_approval(*, is_mutating: bool, mode: ApprovalMode) -> bool:
|
|
19
|
+
return is_mutating and mode is not ApprovalMode.AUTO_ALL
|
rpr/agent/bootstrap.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from rpr.agent.approval import ApprovalMode
|
|
8
|
+
from rpr.agent.client import AgentClient
|
|
9
|
+
from rpr.agent.mock import MockAgentClient
|
|
10
|
+
from rpr.agent.runtime import AgentRuntime, RuntimeCallbacks
|
|
11
|
+
from rpr.agent.session import ChatSession
|
|
12
|
+
from rpr.agent.tools import build_default_tool_registry
|
|
13
|
+
from rpr.context import RunContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True, frozen=True)
|
|
17
|
+
class ChatRuntimeBundle:
|
|
18
|
+
ctx: RunContext
|
|
19
|
+
client: AgentClient
|
|
20
|
+
session: ChatSession
|
|
21
|
+
runtime: AgentRuntime
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_agent_client(provider: str | None, model: str | None) -> AgentClient:
|
|
25
|
+
provider_name = (provider or "mock").lower()
|
|
26
|
+
if provider_name != "mock":
|
|
27
|
+
raise typer.BadParameter(
|
|
28
|
+
"Only the mock provider is available right now.", param_hint="provider"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return MockAgentClient(model_name=model or "mock-1")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_chat_runtime_bundle(
|
|
35
|
+
*,
|
|
36
|
+
ctx: RunContext,
|
|
37
|
+
client: AgentClient,
|
|
38
|
+
approval_mode: ApprovalMode,
|
|
39
|
+
callbacks: RuntimeCallbacks,
|
|
40
|
+
session: ChatSession | None = None,
|
|
41
|
+
) -> ChatRuntimeBundle:
|
|
42
|
+
active_session = session or ChatSession(
|
|
43
|
+
project_dir=ctx.project_dir, approval_mode=approval_mode
|
|
44
|
+
)
|
|
45
|
+
active_session.project_dir = ctx.project_dir
|
|
46
|
+
active_session.approval_mode = approval_mode
|
|
47
|
+
runtime = AgentRuntime(
|
|
48
|
+
client=client,
|
|
49
|
+
session=active_session,
|
|
50
|
+
tools=build_default_tool_registry(),
|
|
51
|
+
ctx=ctx,
|
|
52
|
+
callbacks=callbacks,
|
|
53
|
+
system_prompt=f"Project root: {ctx.project_dir}",
|
|
54
|
+
)
|
|
55
|
+
return ChatRuntimeBundle(
|
|
56
|
+
ctx=ctx,
|
|
57
|
+
client=client,
|
|
58
|
+
session=active_session,
|
|
59
|
+
runtime=runtime,
|
|
60
|
+
)
|
rpr/agent/client.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from rpr.agent.control import TurnControl
|
|
9
|
+
|
|
10
|
+
type MessageRole = Literal["system", "user", "assistant", "tool"]
|
|
11
|
+
type ToolArguments = dict[str, object]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True, frozen=True)
|
|
15
|
+
class TextPart:
|
|
16
|
+
text: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True, frozen=True)
|
|
20
|
+
class FilePart:
|
|
21
|
+
path: str
|
|
22
|
+
mime_type: str
|
|
23
|
+
data: bytes
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_text(self) -> bool:
|
|
27
|
+
return self.mime_type.startswith("text/") or self.mime_type in {
|
|
28
|
+
"application/json",
|
|
29
|
+
"application/xml",
|
|
30
|
+
"application/yaml",
|
|
31
|
+
"application/x-yaml",
|
|
32
|
+
"application/javascript",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def decoded_text(self) -> str | None:
|
|
36
|
+
if not self.is_text:
|
|
37
|
+
return None
|
|
38
|
+
return self.data.decode("utf-8", errors="replace")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
type MessagePart = TextPart | FilePart
|
|
42
|
+
type MessageParts = tuple[MessagePart, ...]
|
|
43
|
+
type MessageContent = str | MessageParts
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True, frozen=True)
|
|
47
|
+
class ConversationMessage:
|
|
48
|
+
role: MessageRole
|
|
49
|
+
content: MessageContent
|
|
50
|
+
name: str | None = None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def parts(self) -> MessageParts:
|
|
54
|
+
if isinstance(self.content, str):
|
|
55
|
+
return (TextPart(self.content),)
|
|
56
|
+
return self.content
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def text_content(self) -> str:
|
|
60
|
+
if isinstance(self.content, str):
|
|
61
|
+
return self.content
|
|
62
|
+
return "".join(part.text for part in self.content if isinstance(part, TextPart))
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def file_parts(self) -> tuple[FilePart, ...]:
|
|
66
|
+
if isinstance(self.content, str):
|
|
67
|
+
return ()
|
|
68
|
+
return tuple(part for part in self.content if isinstance(part, FilePart))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(slots=True, frozen=True)
|
|
72
|
+
class TextDeltaEvent:
|
|
73
|
+
text: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True, frozen=True)
|
|
77
|
+
class ToolCallRequestEvent:
|
|
78
|
+
call_id: str
|
|
79
|
+
tool_name: str
|
|
80
|
+
arguments: ToolArguments
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(slots=True, frozen=True)
|
|
84
|
+
class TurnCompleteEvent:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
type AgentEvent = TextDeltaEvent | ToolCallRequestEvent | TurnCompleteEvent
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AgentClient(ABC):
|
|
92
|
+
"""Abstract base class for AI agent clients."""
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def provider_name(self) -> str:
|
|
97
|
+
"""Return the name of the provider."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def model_name(self) -> str:
|
|
103
|
+
"""Return the active model name."""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
async def stream(
|
|
108
|
+
self,
|
|
109
|
+
history: list[ConversationMessage],
|
|
110
|
+
*,
|
|
111
|
+
system_prompt: str | None = None,
|
|
112
|
+
control: TurnControl | None = None,
|
|
113
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
114
|
+
"""Stream provider-agnostic events for the current turn."""
|
|
115
|
+
...
|
rpr/agent/control.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from threading import Event
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(slots=True)
|
|
8
|
+
class TurnControl:
|
|
9
|
+
_cancel_requested: Event = field(default_factory=Event)
|
|
10
|
+
|
|
11
|
+
def cancel(self) -> None:
|
|
12
|
+
self._cancel_requested.set()
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def is_cancelled(self) -> bool:
|
|
16
|
+
return self._cancel_requested.is_set()
|
rpr/agent/mock.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import AsyncGenerator, Iterator
|
|
6
|
+
from typing import override
|
|
7
|
+
|
|
8
|
+
from rpr.agent.client import (
|
|
9
|
+
AgentClient,
|
|
10
|
+
ConversationMessage,
|
|
11
|
+
FilePart,
|
|
12
|
+
TextDeltaEvent,
|
|
13
|
+
ToolArguments,
|
|
14
|
+
ToolCallRequestEvent,
|
|
15
|
+
TurnCompleteEvent,
|
|
16
|
+
)
|
|
17
|
+
from rpr.agent.control import TurnControl
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MockAgentClient(AgentClient):
|
|
21
|
+
"""A mock agent client for testing and default behavior."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, *, model_name: str = "mock-1") -> None:
|
|
24
|
+
self._model_name = model_name
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
@override
|
|
28
|
+
def provider_name(self) -> str:
|
|
29
|
+
return "mock"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@override
|
|
33
|
+
def model_name(self) -> str:
|
|
34
|
+
return self._model_name
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
async def stream(
|
|
38
|
+
self,
|
|
39
|
+
history: list[ConversationMessage],
|
|
40
|
+
*,
|
|
41
|
+
system_prompt: str | None = None,
|
|
42
|
+
control: TurnControl | None = None,
|
|
43
|
+
) -> AsyncGenerator[
|
|
44
|
+
TextDeltaEvent | ToolCallRequestEvent | TurnCompleteEvent, None
|
|
45
|
+
]:
|
|
46
|
+
del system_prompt
|
|
47
|
+
last_message = history[-1]
|
|
48
|
+
if last_message.role == "tool":
|
|
49
|
+
async for event in self._stream_text(
|
|
50
|
+
f"I used `{last_message.name}` and summarized the result below.\n\n{last_message.text_content}",
|
|
51
|
+
control=control,
|
|
52
|
+
):
|
|
53
|
+
yield event
|
|
54
|
+
if control is None or not control.is_cancelled:
|
|
55
|
+
yield TurnCompleteEvent()
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if last_message.role != "user":
|
|
59
|
+
async for event in self._stream_text(
|
|
60
|
+
"I am ready for the next prompt.", control=control
|
|
61
|
+
):
|
|
62
|
+
yield event
|
|
63
|
+
if control is None or not control.is_cancelled:
|
|
64
|
+
yield TurnCompleteEvent()
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
tool_request = self._parse_tool_request(last_message.text_content)
|
|
68
|
+
if tool_request is not None:
|
|
69
|
+
async for event in self._stream_text(
|
|
70
|
+
"I’m going to inspect the repository before answering.\n\n",
|
|
71
|
+
control=control,
|
|
72
|
+
):
|
|
73
|
+
yield event
|
|
74
|
+
if control is None or not control.is_cancelled:
|
|
75
|
+
yield ToolCallRequestEvent(
|
|
76
|
+
call_id=f"mock-{len(history)}",
|
|
77
|
+
tool_name=tool_request[0],
|
|
78
|
+
arguments=tool_request[1],
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
response_text = self._render_response_text(last_message)
|
|
83
|
+
async for event in self._stream_text(response_text, control=control):
|
|
84
|
+
yield event
|
|
85
|
+
if control is None or not control.is_cancelled:
|
|
86
|
+
yield TurnCompleteEvent()
|
|
87
|
+
|
|
88
|
+
async def _stream_text(
|
|
89
|
+
self,
|
|
90
|
+
text: str,
|
|
91
|
+
*,
|
|
92
|
+
control: TurnControl | None = None,
|
|
93
|
+
) -> AsyncGenerator[TextDeltaEvent, None]:
|
|
94
|
+
for chunk in self._iter_chunks(text):
|
|
95
|
+
if control is not None and control.is_cancelled:
|
|
96
|
+
return
|
|
97
|
+
await asyncio.sleep(0.1)
|
|
98
|
+
yield TextDeltaEvent(text=chunk)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _iter_chunks(text: str) -> Iterator[str]:
|
|
102
|
+
for match in re.finditer(r"\n+|[^\s\n]+\s*", text):
|
|
103
|
+
yield match.group(0)
|
|
104
|
+
|
|
105
|
+
def _parse_tool_request(self, message: str) -> tuple[str, ToolArguments] | None:
|
|
106
|
+
read_match = re.search(
|
|
107
|
+
r"(?:read|show) file (?P<path>\S+)", message, re.IGNORECASE
|
|
108
|
+
)
|
|
109
|
+
if read_match:
|
|
110
|
+
return "read_file", {"path": read_match.group("path")}
|
|
111
|
+
|
|
112
|
+
list_match = re.search(
|
|
113
|
+
r"(?:list|show) (?:directory|files)(?: (?P<path>\S+))?",
|
|
114
|
+
message,
|
|
115
|
+
re.IGNORECASE,
|
|
116
|
+
)
|
|
117
|
+
if list_match:
|
|
118
|
+
return "list_directory", {"path": list_match.group("path") or "."}
|
|
119
|
+
|
|
120
|
+
search_match = re.search(
|
|
121
|
+
r"search(?: for)? (?P<query>.+)", message, re.IGNORECASE
|
|
122
|
+
)
|
|
123
|
+
if search_match:
|
|
124
|
+
return "search_text", {"query": search_match.group("query").strip()}
|
|
125
|
+
|
|
126
|
+
write_match = re.search(
|
|
127
|
+
r"write file (?P<path>\S+)\s*:(?P<content>.+)",
|
|
128
|
+
message,
|
|
129
|
+
re.IGNORECASE | re.DOTALL,
|
|
130
|
+
)
|
|
131
|
+
if write_match:
|
|
132
|
+
return "write_file", {
|
|
133
|
+
"path": write_match.group("path"),
|
|
134
|
+
"content": write_match.group("content").lstrip(),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def _render_response_text(self, message: ConversationMessage) -> str:
|
|
140
|
+
response_text = (
|
|
141
|
+
f"I received your message: '{message.text_content}'.\n\n"
|
|
142
|
+
f"This is a mock response from the **Mock Provider** using `{self.model_name}`."
|
|
143
|
+
)
|
|
144
|
+
if not message.file_parts:
|
|
145
|
+
return response_text
|
|
146
|
+
|
|
147
|
+
attachment_lines = [
|
|
148
|
+
self._format_attachment(part) for part in message.file_parts
|
|
149
|
+
]
|
|
150
|
+
return f"{response_text}\n\nAttachments:\n" + "\n".join(attachment_lines)
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _format_attachment(part: FilePart) -> str:
|
|
154
|
+
if part.is_text:
|
|
155
|
+
preview = (part.decoded_text() or "").splitlines()
|
|
156
|
+
if preview:
|
|
157
|
+
return f"- {part.path} ({part.mime_type}, {len(part.data)} bytes): {preview[0][:80]}"
|
|
158
|
+
return f"- {part.path} ({part.mime_type}, {len(part.data)} bytes)"
|