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.
Files changed (213) hide show
  1. rpr/__init__.py +1 -0
  2. rpr/_native.pyd +0 -0
  3. rpr/_native.pyi +32 -0
  4. rpr/agent/__init__.py +29 -0
  5. rpr/agent/approval.py +19 -0
  6. rpr/agent/bootstrap.py +60 -0
  7. rpr/agent/client.py +115 -0
  8. rpr/agent/control.py +16 -0
  9. rpr/agent/mock.py +158 -0
  10. rpr/agent/runtime.py +291 -0
  11. rpr/agent/session.py +91 -0
  12. rpr/agent/tools/__init__.py +10 -0
  13. rpr/agent/tools/base.py +62 -0
  14. rpr/agent/tools/mutating.py +47 -0
  15. rpr/agent/tools/readonly.py +147 -0
  16. rpr/agent/tools/registry.py +30 -0
  17. rpr/application/__init__.py +1 -0
  18. rpr/application/catalog.py +485 -0
  19. rpr/application/chat_service.py +413 -0
  20. rpr/application/checks.py +49 -0
  21. rpr/application/cli_adapter.py +36 -0
  22. rpr/application/completer.py +163 -0
  23. rpr/application/conversation_service.py +92 -0
  24. rpr/application/prompt_service.py +85 -0
  25. rpr/application/selector.py +240 -0
  26. rpr/application/shell.py +541 -0
  27. rpr/checks/__init__.py +0 -0
  28. rpr/checks/base.py +13 -0
  29. rpr/checks/instructions.py +142 -0
  30. rpr/checks/packages.py +619 -0
  31. rpr/checks/workspace.py +176 -0
  32. rpr/cli.py +94 -0
  33. rpr/commands/__init__.py +0 -0
  34. rpr/commands/add.py +624 -0
  35. rpr/commands/chat.py +48 -0
  36. rpr/commands/check.py +53 -0
  37. rpr/commands/generate/__init__.py +0 -0
  38. rpr/commands/generate/api.py +249 -0
  39. rpr/commands/generate/domain.py +434 -0
  40. rpr/commands/generate/engine.py +156 -0
  41. rpr/commands/generate/storybook.py +412 -0
  42. rpr/commands/generate/ui.py +481 -0
  43. rpr/commands/init.py +951 -0
  44. rpr/commands/map.py +113 -0
  45. rpr/commands/refresh.py +65 -0
  46. rpr/commands/rename.py +104 -0
  47. rpr/commands/settings.py +102 -0
  48. rpr/commands/sync.py +121 -0
  49. rpr/commands/update.py +222 -0
  50. rpr/context.py +275 -0
  51. rpr/dependencies.py +212 -0
  52. rpr/dependency_manifest_data.py +270 -0
  53. rpr/generators/__init__.py +0 -0
  54. rpr/generators/base.py +134 -0
  55. rpr/generators/claude.py +57 -0
  56. rpr/generators/codex.py +46 -0
  57. rpr/generators/copilot.py +31 -0
  58. rpr/generators/cursor.py +38 -0
  59. rpr/generators/gemini.py +33 -0
  60. rpr/generators/github.py +35 -0
  61. rpr/manifest_refresh.py +212 -0
  62. rpr/map/__init__.py +0 -0
  63. rpr/map/architecture.py +495 -0
  64. rpr/map/chains.py +317 -0
  65. rpr/map/classifier.py +170 -0
  66. rpr/map/coverage.py +200 -0
  67. rpr/map/dependencies.py +243 -0
  68. rpr/map/extractor.py +223 -0
  69. rpr/map/graph.py +318 -0
  70. rpr/map/output.py +1030 -0
  71. rpr/map/responsibility.py +345 -0
  72. rpr/map/topology.py +327 -0
  73. rpr/map/walker.py +151 -0
  74. rpr/package_updates.py +731 -0
  75. rpr/project_rename.py +129 -0
  76. rpr/scaffolds/domain/base_entity.md +30 -0
  77. rpr/scaffolds/domain/base_repo.md +48 -0
  78. rpr/scaffolds/domain/container.md +74 -0
  79. rpr/scaffolds/domain/settings.md +91 -0
  80. rpr/scaffolds/instructions/all.instructions.md +49 -0
  81. rpr/scaffolds/instructions/api.instructions.md +42 -0
  82. rpr/scaffolds/instructions/domain.instructions.md +96 -0
  83. rpr/scaffolds/instructions/frontend.instructions.md +110 -0
  84. rpr/scaffolds/instructions/rust-engine.instructions.md +41 -0
  85. rpr/scaffolds/instructions/setup-guide.instructions.md +89 -0
  86. rpr/scaffolds/instructions/tooling-setup.instructions.md +91 -0
  87. rpr/scaffolds/instructions/tooling.instructions.md +43 -0
  88. rpr/scaffolds/js_special_files/chat/BACKEND_FLOW.md +321 -0
  89. rpr/scaffolds/js_special_files/chat/README.md +623 -0
  90. rpr/scaffolds/js_special_files/chat/client/content.ts +47 -0
  91. rpr/scaffolds/js_special_files/chat/client/httpChatStreamClient.spec.ts +227 -0
  92. rpr/scaffolds/js_special_files/chat/client/httpChatStreamClient.ts +201 -0
  93. rpr/scaffolds/js_special_files/chat/client/index.ts +2 -0
  94. rpr/scaffolds/js_special_files/chat/client/types.ts +74 -0
  95. rpr/scaffolds/js_special_files/chat/composer/ChatComposer.spec.tsx +363 -0
  96. rpr/scaffolds/js_special_files/chat/composer/ChatComposer.tsx +444 -0
  97. rpr/scaffolds/js_special_files/chat/composer/__screenshots__/ChatComposer.spec.tsx/ChatComposer-renders-custom-action-buttons-beside-the-attachment-control-1.png +0 -0
  98. rpr/scaffolds/js_special_files/chat/composer/fileDrop.spec.ts +60 -0
  99. rpr/scaffolds/js_special_files/chat/composer/fileDrop.ts +128 -0
  100. rpr/scaffolds/js_special_files/chat/composer/index.ts +6 -0
  101. rpr/scaffolds/js_special_files/chat/composer/suggestions/ChatComposerMentionTokens.tsx +128 -0
  102. rpr/scaffolds/js_special_files/chat/composer/suggestions/ChatSuggestionsPopover.tsx +125 -0
  103. rpr/scaffolds/js_special_files/chat/composer/suggestions/LocalFileSuggestionAction.tsx +100 -0
  104. rpr/scaffolds/js_special_files/chat/composer/suggestions/index.ts +7 -0
  105. rpr/scaffolds/js_special_files/chat/composer/suggestions/localFileSuggestions.spec.ts +282 -0
  106. rpr/scaffolds/js_special_files/chat/composer/suggestions/localFileSuggestions.ts +1004 -0
  107. rpr/scaffolds/js_special_files/chat/composer/suggestions/triggerDetection.spec.ts +56 -0
  108. rpr/scaffolds/js_special_files/chat/composer/suggestions/triggerDetection.ts +58 -0
  109. rpr/scaffolds/js_special_files/chat/composer/suggestions/types.ts +124 -0
  110. rpr/scaffolds/js_special_files/chat/composer/suggestions/useChatSuggestions.ts +330 -0
  111. rpr/scaffolds/js_special_files/chat/composer/types.ts +166 -0
  112. rpr/scaffolds/js_special_files/chat/composer/useChatComposer.ts +249 -0
  113. rpr/scaffolds/js_special_files/chat/composer/useChatStream.spec.tsx +312 -0
  114. rpr/scaffolds/js_special_files/chat/composer/useChatStream.ts +527 -0
  115. rpr/scaffolds/js_special_files/chat/experience/ChatExperience.spec.tsx +142 -0
  116. rpr/scaffolds/js_special_files/chat/experience/ChatExperience.styles.tsx +101 -0
  117. rpr/scaffolds/js_special_files/chat/experience/ChatExperience.tsx +166 -0
  118. rpr/scaffolds/js_special_files/chat/experience/ChatExperienceComposer.styles.tsx +101 -0
  119. rpr/scaffolds/js_special_files/chat/experience/ChatExperienceComposer.tsx +176 -0
  120. rpr/scaffolds/js_special_files/chat/experience/ChatExperienceHeader.styles.tsx +29 -0
  121. rpr/scaffolds/js_special_files/chat/experience/ChatExperienceHeader.tsx +45 -0
  122. rpr/scaffolds/js_special_files/chat/experience/ThreadHistory.styles.tsx +80 -0
  123. rpr/scaffolds/js_special_files/chat/experience/ThreadHistory.tsx +127 -0
  124. rpr/scaffolds/js_special_files/chat/experience/chatStorage.spec.ts +98 -0
  125. rpr/scaffolds/js_special_files/chat/experience/chatStorage.ts +128 -0
  126. rpr/scaffolds/js_special_files/chat/experience/index.ts +7 -0
  127. rpr/scaffolds/js_special_files/chat/experience/types.ts +108 -0
  128. rpr/scaffolds/js_special_files/chat/experience/useChatExperience.spec.tsx +148 -0
  129. rpr/scaffolds/js_special_files/chat/experience/useChatExperience.ts +473 -0
  130. rpr/scaffolds/js_special_files/chat/index.ts +5 -0
  131. rpr/scaffolds/js_special_files/chat/testing/createMockChatStreamClient.ts +153 -0
  132. rpr/scaffolds/js_special_files/chat/testing/createMockSuggestionProvider.ts +103 -0
  133. rpr/scaffolds/js_special_files/chat/testing/index.ts +2 -0
  134. rpr/scaffolds/js_special_files/chat/thread/ChatMessage.tsx +268 -0
  135. rpr/scaffolds/js_special_files/chat/thread/ChatMessageActions.tsx +139 -0
  136. rpr/scaffolds/js_special_files/chat/thread/ChatMessageContent.spec.tsx +163 -0
  137. rpr/scaffolds/js_special_files/chat/thread/ChatMessageContent.tsx +690 -0
  138. rpr/scaffolds/js_special_files/chat/thread/ChatThread.spec.tsx +365 -0
  139. rpr/scaffolds/js_special_files/chat/thread/ChatThread.tsx +172 -0
  140. rpr/scaffolds/js_special_files/chat/thread/README.md +111 -0
  141. rpr/scaffolds/js_special_files/chat/thread/contentFormat.ts +30 -0
  142. rpr/scaffolds/js_special_files/chat/thread/index.ts +7 -0
  143. rpr/scaffolds/js_special_files/chat/thread/streaming.ts +176 -0
  144. rpr/scaffolds/js_special_files/chat/thread/types.ts +189 -0
  145. rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponent.spec.tsx +147 -0
  146. rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponent.tsx +94 -0
  147. rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponentList.spec.tsx +277 -0
  148. rpr/scaffolds/js_special_files/chat/virtual-list/VirtualComponentList.tsx +121 -0
  149. rpr/scaffolds/js_special_files/chat/virtual-list/index.ts +1 -0
  150. rpr/scaffolds/js_special_files/fetch.service.md +222 -0
  151. rpr/scaffolds/js_special_files/sticky-navigation.md +170 -0
  152. rpr/scaffolds/js_special_files/sticky-navigation.spec.md +231 -0
  153. rpr/scaffolds/js_special_files/vitest-browser-config.md +24 -0
  154. rpr/scaffolds/py_special_files/chat/api/routes/chat_routes.py +242 -0
  155. rpr/scaffolds/py_special_files/chat/api/tests/test_chat.py +287 -0
  156. rpr/scaffolds/py_special_files/chat/domain/models/dtos/chat_dtos.py +450 -0
  157. rpr/scaffolds/py_special_files/chat/domain/models/dtos/conversation_history_dtos.py +37 -0
  158. rpr/scaffolds/py_special_files/chat/domain/models/entities/conversation_history.py +122 -0
  159. rpr/scaffolds/py_special_files/chat/domain/repos/conversation_history.py +349 -0
  160. rpr/scaffolds/py_special_files/chat/domain/services/agent_session_codec.py +53 -0
  161. rpr/scaffolds/py_special_files/chat/domain/services/chat_service.py +222 -0
  162. rpr/scaffolds/py_special_files/chat/domain/services/conversation_history_service.py +192 -0
  163. rpr/scaffolds/py_special_files/chat/domain/services/conversation_operations.py +148 -0
  164. rpr/scaffolds/py_special_files/chat/domain/services/history_codec.py +146 -0
  165. rpr/scaffolds/py_special_files/chat/domain/services/in_memory_history_store.py +94 -0
  166. rpr/scaffolds/py_special_files/chat/domain/services/postgres_history_store.py +143 -0
  167. rpr/scaffolds/py_special_files/chat/domain/services/protocols/conversation_history_store.py +56 -0
  168. rpr/scaffolds/py_special_files/chat/domain/services/protocols/conversation_runtime.py +90 -0
  169. rpr/scaffolds/py_special_files/chat/domain/services/protocols/llm_provider.py +52 -0
  170. rpr/scaffolds/py_special_files/chat/domain/tests/test_chat_dtos.py +110 -0
  171. rpr/scaffolds/py_special_files/chat/domain/tests/test_chat_service.py +401 -0
  172. rpr/scaffolds/py_special_files/chat/domain/tests/test_history_codec.py +122 -0
  173. rpr/scaffolds/py_special_files/chat/domain/tests/test_history_projection.py +19 -0
  174. rpr/scaffolds/py_special_files/chat/domain/tests/test_persistence_config.py +24 -0
  175. rpr/scaffolds/py_special_files/chat/domain/tests/test_postgres_history_store.py +165 -0
  176. rpr/scaffolds/py_special_files/vllm_provider/domain/models/dtos/__init__.py +3 -0
  177. rpr/scaffolds/py_special_files/vllm_provider/domain/models/dtos/vllm_model_dtos.py +113 -0
  178. rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/VLLM_SETUP.md +31 -0
  179. rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/__init__.py +18 -0
  180. rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/model_catalog.py +84 -0
  181. rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/vllm_agent_factory.py +129 -0
  182. rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/vllm_agent_factory_protocol.py +18 -0
  183. rpr/scaffolds/py_special_files/vllm_provider/domain/services/agents/vllm_inference_coordinator.py +207 -0
  184. rpr/scaffolds/py_special_files/vllm_provider/domain/services/runtimes/__init__.py +3 -0
  185. rpr/scaffolds/py_special_files/vllm_provider/domain/services/runtimes/agents_sdk.py +211 -0
  186. rpr/scaffolds/py_special_files/vllm_provider/domain/services/runtimes/agents_sdk_message_mapping.py +120 -0
  187. rpr/scaffolds/py_special_files/vllm_provider/domain/tests/test_agents_sdk_runtime.py +205 -0
  188. rpr/scaffolds/py_special_files/vllm_provider/domain/tests/test_vllm_model_catalog.py +768 -0
  189. rpr/scaffolds/py_special_files/vllm_provider/domain/tests/test_vllm_provider.py +110 -0
  190. rpr/scaffolds/special_files/camel_case_model.md +60 -0
  191. rpr/scaffolds/special_files/domain_container.md +120 -0
  192. rpr/scaffolds/special_files/domain_settings.md +74 -0
  193. rpr/scaffolds/special_files/dto_util.md +62 -0
  194. rpr/scaffolds/special_files/encrypted_column.md +119 -0
  195. rpr/scaffolds/special_files/logging.md +32 -0
  196. rpr/scaffolds/special_files/mapper_util.md +159 -0
  197. rpr/scaffolds/special_files/partial_update.md +61 -0
  198. rpr/scaffolds/special_files/test_encrypted_column.md +103 -0
  199. rpr/templates/__init__.py +0 -0
  200. rpr/templates/registry.py +414 -0
  201. rpr/ui/__init__.py +0 -0
  202. rpr/ui/console.py +32 -0
  203. rpr/ui/markdown.py +59 -0
  204. rpr/ui/prompt_session.py +430 -0
  205. rpr/ui/renderers.py +167 -0
  206. rpr/ui/theme.py +286 -0
  207. rpr/workspace.py +139 -0
  208. rpr_cli-0.3.7.dist-info/METADATA +283 -0
  209. rpr_cli-0.3.7.dist-info/RECORD +213 -0
  210. rpr_cli-0.3.7.dist-info/WHEEL +4 -0
  211. rpr_cli-0.3.7.dist-info/entry_points.txt +2 -0
  212. rpr_cli-0.3.7.dist-info/licenses/LICENSE +21 -0
  213. 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)"