goose-py 0.9.16__tar.gz → 0.10.0__tar.gz

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 (39) hide show
  1. {goose_py-0.9.16 → goose_py-0.10.0}/PKG-INFO +1 -1
  2. goose_py-0.10.0/goose/_internal/agent.py +245 -0
  3. goose_py-0.10.0/goose/_internal/result.py +20 -0
  4. {goose_py-0.9.16 → goose_py-0.10.0}/goose/_internal/task.py +3 -2
  5. goose_py-0.10.0/goose/_internal/types/telemetry.py +113 -0
  6. {goose_py-0.9.16 → goose_py-0.10.0}/pyproject.toml +2 -1
  7. {goose_py-0.9.16 → goose_py-0.10.0}/uv.lock +439 -1
  8. goose_py-0.9.16/goose/_internal/agent.py +0 -179
  9. goose_py-0.9.16/goose/_internal/result.py +0 -9
  10. {goose_py-0.9.16 → goose_py-0.10.0}/.envrc +0 -0
  11. {goose_py-0.9.16 → goose_py-0.10.0}/.github/workflows/publish.yml +0 -0
  12. {goose_py-0.9.16 → goose_py-0.10.0}/.gitignore +0 -0
  13. {goose_py-0.9.16 → goose_py-0.10.0}/.python-version +0 -0
  14. {goose_py-0.9.16 → goose_py-0.10.0}/.stubs/jsonpath_ng/__init__.pyi +0 -0
  15. {goose_py-0.9.16 → goose_py-0.10.0}/.stubs/litellm/__init__.pyi +0 -0
  16. {goose_py-0.9.16 → goose_py-0.10.0}/Makefile +0 -0
  17. {goose_py-0.9.16 → goose_py-0.10.0}/README.md +0 -0
  18. {goose_py-0.9.16 → goose_py-0.10.0}/goose/__init__.py +0 -0
  19. {goose_py-0.9.16 → goose_py-0.10.0}/goose/_internal/conversation.py +0 -0
  20. {goose_py-0.9.16 → goose_py-0.10.0}/goose/_internal/flow.py +0 -0
  21. {goose_py-0.9.16 → goose_py-0.10.0}/goose/_internal/state.py +0 -0
  22. {goose_py-0.9.16 → goose_py-0.10.0}/goose/_internal/store.py +0 -0
  23. {goose_py-0.9.16 → goose_py-0.10.0}/goose/_internal/types/__init__.py +0 -0
  24. {goose_py-0.9.16 → goose_py-0.10.0}/goose/_internal/types/agent.py +0 -0
  25. {goose_py-0.9.16 → goose_py-0.10.0}/goose/agent.py +0 -0
  26. {goose_py-0.9.16 → goose_py-0.10.0}/goose/errors.py +0 -0
  27. {goose_py-0.9.16 → goose_py-0.10.0}/goose/flow.py +0 -0
  28. {goose_py-0.9.16 → goose_py-0.10.0}/goose/py.typed +0 -0
  29. {goose_py-0.9.16 → goose_py-0.10.0}/goose/runs.py +0 -0
  30. {goose_py-0.9.16 → goose_py-0.10.0}/goose/task.py +0 -0
  31. {goose_py-0.9.16 → goose_py-0.10.0}/tests/__init__.py +0 -0
  32. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_agent.py +0 -0
  33. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_ask.py +0 -0
  34. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_downstream_task.py +0 -0
  35. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_hashing.py +0 -0
  36. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_looping.py +0 -0
  37. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_refining.py +0 -0
  38. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_regenerate.py +0 -0
  39. {goose_py-0.9.16 → goose_py-0.10.0}/tests/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: goose-py
3
- Version: 0.9.16
3
+ Version: 0.10.0
4
4
  Summary: A tool for AI workflows based on human-computer collaboration and structured output.
5
5
  Author-email: Nash Taylor <nash@chelle.ai>, Joshua Cook <joshua@chelle.ai>, Michael Sankur <michael@chelle.ai>
6
6
  Requires-Python: >=3.12
@@ -0,0 +1,245 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from typing import Any, Literal, Protocol, overload
4
+
5
+ from litellm import acompletion
6
+ from pydantic import ValidationError
7
+
8
+ from goose._internal.types.telemetry import AgentResponse
9
+ from goose.errors import Honk
10
+
11
+ from .result import FindReplaceResponse, Result, TextResult
12
+ from .types.agent import AIModel, LLMMessage
13
+
14
+
15
+ class IAgentLogger(Protocol):
16
+ async def __call__(self, *, response: AgentResponse[Any]) -> None: ...
17
+
18
+
19
+ class Agent:
20
+ def __init__(
21
+ self,
22
+ *,
23
+ flow_name: str,
24
+ run_id: str,
25
+ logger: IAgentLogger | None = None,
26
+ ) -> None:
27
+ self.flow_name = flow_name
28
+ self.run_id = run_id
29
+ self.logger = logger
30
+
31
+ async def generate[R: Result](
32
+ self,
33
+ *,
34
+ messages: list[LLMMessage],
35
+ model: AIModel,
36
+ task_name: str,
37
+ response_model: type[R] = TextResult,
38
+ system: LLMMessage | None = None,
39
+ ) -> R:
40
+ start_time = datetime.now()
41
+ if system is not None:
42
+ messages.insert(0, system)
43
+
44
+ if response_model is TextResult:
45
+ response = await acompletion(model=model.value, messages=messages)
46
+ parsed_response = response_model.model_validate({"text": response.choices[0].message.content})
47
+ else:
48
+ response = await acompletion(
49
+ model=model.value,
50
+ messages=messages,
51
+ response_format=response_model,
52
+ )
53
+ parsed_response = response_model.model_validate_json(response.choices[0].message.content)
54
+
55
+ end_time = datetime.now()
56
+ agent_response = AgentResponse(
57
+ response=parsed_response,
58
+ run_id=self.run_id,
59
+ flow_name=self.flow_name,
60
+ task_name=task_name,
61
+ model=model,
62
+ system=system,
63
+ input_messages=messages,
64
+ input_tokens=response.usage.prompt_tokens,
65
+ output_tokens=response.usage.completion_tokens,
66
+ start_time=start_time,
67
+ end_time=end_time,
68
+ )
69
+
70
+ if self.logger is not None:
71
+ await self.logger(response=agent_response)
72
+ else:
73
+ logging.info(agent_response.model_dump())
74
+
75
+ return parsed_response
76
+
77
+ async def ask(
78
+ self, *, messages: list[LLMMessage], model: AIModel, task_name: str, system: LLMMessage | None = None
79
+ ) -> str:
80
+ start_time = datetime.now()
81
+
82
+ if system is not None:
83
+ messages.insert(0, system)
84
+ response = await acompletion(model=model.value, messages=messages)
85
+
86
+ end_time = datetime.now()
87
+ agent_response = AgentResponse(
88
+ response=response.choices[0].message.content,
89
+ run_id=self.run_id,
90
+ flow_name=self.flow_name,
91
+ task_name=task_name,
92
+ model=model,
93
+ system=system,
94
+ input_messages=messages,
95
+ input_tokens=response.usage.prompt_tokens,
96
+ output_tokens=response.usage.completion_tokens,
97
+ start_time=start_time,
98
+ end_time=end_time,
99
+ )
100
+
101
+ if self.logger is not None:
102
+ await self.logger(response=agent_response)
103
+ else:
104
+ logging.info(agent_response.model_dump())
105
+
106
+ return response.choices[0].message.content
107
+
108
+ async def refine[R: Result](
109
+ self,
110
+ *,
111
+ messages: list[LLMMessage],
112
+ model: AIModel,
113
+ task_name: str,
114
+ response_model: type[R],
115
+ system: LLMMessage | None = None,
116
+ ) -> R:
117
+ start_time = datetime.now()
118
+
119
+ if system is not None:
120
+ messages.insert(0, system)
121
+
122
+ find_replace_response = await acompletion(
123
+ model=model.value, messages=messages, response_format=FindReplaceResponse
124
+ )
125
+ parsed_find_replace_response = FindReplaceResponse.model_validate_json(
126
+ find_replace_response.choices[0].message.content
127
+ )
128
+
129
+ end_time = datetime.now()
130
+ agent_response = AgentResponse(
131
+ response=parsed_find_replace_response,
132
+ run_id=self.run_id,
133
+ flow_name=self.flow_name,
134
+ task_name=task_name,
135
+ model=model,
136
+ system=system,
137
+ input_messages=messages,
138
+ input_tokens=find_replace_response.usage.prompt_tokens,
139
+ output_tokens=find_replace_response.usage.completion_tokens,
140
+ start_time=start_time,
141
+ end_time=end_time,
142
+ )
143
+
144
+ if self.logger is not None:
145
+ await self.logger(response=agent_response)
146
+ else:
147
+ logging.info(agent_response.model_dump())
148
+
149
+ refined_response = self.__apply_find_replace(
150
+ result=self.__find_last_result(messages=messages, response_model=response_model),
151
+ find_replace_response=parsed_find_replace_response,
152
+ response_model=response_model,
153
+ )
154
+
155
+ return refined_response
156
+
157
+ @overload
158
+ async def __call__[R: Result](
159
+ self,
160
+ *,
161
+ messages: list[LLMMessage],
162
+ model: AIModel,
163
+ task_name: str,
164
+ mode: Literal["generate"],
165
+ response_model: type[R],
166
+ system: LLMMessage | None = None,
167
+ ) -> R: ...
168
+
169
+ @overload
170
+ async def __call__[R: Result](
171
+ self,
172
+ *,
173
+ messages: list[LLMMessage],
174
+ model: AIModel,
175
+ task_name: str,
176
+ mode: Literal["ask"],
177
+ response_model: type[R] = TextResult,
178
+ system: LLMMessage | None = None,
179
+ ) -> str: ...
180
+
181
+ @overload
182
+ async def __call__[R: Result](
183
+ self,
184
+ *,
185
+ messages: list[LLMMessage],
186
+ model: AIModel,
187
+ task_name: str,
188
+ response_model: type[R],
189
+ mode: Literal["refine"],
190
+ system: LLMMessage | None = None,
191
+ ) -> R: ...
192
+
193
+ @overload
194
+ async def __call__[R: Result](
195
+ self,
196
+ *,
197
+ messages: list[LLMMessage],
198
+ model: AIModel,
199
+ task_name: str,
200
+ response_model: type[R],
201
+ system: LLMMessage | None = None,
202
+ ) -> R: ...
203
+
204
+ async def __call__[R: Result](
205
+ self,
206
+ *,
207
+ messages: list[LLMMessage],
208
+ model: AIModel,
209
+ task_name: str,
210
+ response_model: type[R] = TextResult,
211
+ mode: Literal["generate", "ask", "refine"] = "generate",
212
+ system: LLMMessage | None = None,
213
+ ) -> R | str:
214
+ match mode:
215
+ case "generate":
216
+ return await self.generate(
217
+ messages=messages, model=model, task_name=task_name, response_model=response_model, system=system
218
+ )
219
+ case "ask":
220
+ return await self.ask(messages=messages, model=model, task_name=task_name, system=system)
221
+ case "refine":
222
+ return await self.refine(
223
+ messages=messages, model=model, task_name=task_name, response_model=response_model, system=system
224
+ )
225
+
226
+ def __apply_find_replace[R: Result](
227
+ self, *, result: R, find_replace_response: FindReplaceResponse, response_model: type[R]
228
+ ) -> R:
229
+ dumped_result = result.model_dump_json()
230
+ for replacement in find_replace_response.replacements:
231
+ dumped_result = dumped_result.replace(replacement.find, replacement.replace)
232
+
233
+ return response_model.model_validate_json(dumped_result)
234
+
235
+ def __find_last_result[R: Result](self, *, messages: list[LLMMessage], response_model: type[R]) -> R:
236
+ for message in reversed(messages):
237
+ if message["role"] == "assistant":
238
+ try:
239
+ only_part = message["content"][0]
240
+ if only_part["type"] == "text":
241
+ return response_model.model_validate_json(only_part["text"])
242
+ except ValidationError:
243
+ continue
244
+
245
+ raise Honk("No last result found, failed to refine")
@@ -0,0 +1,20 @@
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+
4
+ class Result(BaseModel):
5
+ model_config = ConfigDict(frozen=True)
6
+
7
+
8
+ class TextResult(Result):
9
+ text: str
10
+
11
+
12
+ class Replacement(BaseModel):
13
+ find: str = Field(description="Text to find, to be replaced with `replace`")
14
+ replace: str = Field(description="Text to replace `find` with")
15
+
16
+
17
+ class FindReplaceResponse(BaseModel):
18
+ replacements: list[Replacement] = Field(
19
+ description="List of replacements to make in the previous result to satisfy the user's request"
20
+ )
@@ -57,11 +57,12 @@ class Task[**P, R: Result]:
57
57
  model=self._refinement_model,
58
58
  task_name=f"ask--{self.name}",
59
59
  system=context.render() if context is not None else None,
60
+ mode="ask",
60
61
  )
61
- node_state.add_answer(answer=answer.text)
62
+ node_state.add_answer(answer=answer)
62
63
  flow_run.upsert_node_state(node_state)
63
64
 
64
- return answer.text
65
+ return answer
65
66
 
66
67
  async def refine(
67
68
  self,
@@ -0,0 +1,113 @@
1
+ import json
2
+ from datetime import datetime
3
+ from typing import ClassVar, TypedDict
4
+
5
+ from pydantic import BaseModel, computed_field
6
+
7
+ from ..types.agent import AIModel, LLMMessage
8
+
9
+
10
+ class AgentResponseDump(TypedDict):
11
+ run_id: str
12
+ flow_name: str
13
+ task_name: str
14
+ model: str
15
+ system_message: str
16
+ input_messages: list[str]
17
+ output_message: str
18
+ input_cost: float
19
+ output_cost: float
20
+ total_cost: float
21
+ input_tokens: int
22
+ output_tokens: int
23
+ start_time: datetime
24
+ end_time: datetime
25
+ duration_ms: int
26
+
27
+
28
+ class AgentResponse[R: BaseModel | str](BaseModel):
29
+ INPUT_DOLLARS_PER_MILLION_TOKENS: ClassVar[dict[AIModel, float]] = {
30
+ AIModel.VERTEX_FLASH_8B: 0.30,
31
+ AIModel.VERTEX_FLASH: 0.15,
32
+ AIModel.VERTEX_PRO: 5.00,
33
+ AIModel.GEMINI_FLASH_8B: 0.30,
34
+ AIModel.GEMINI_FLASH: 0.15,
35
+ AIModel.GEMINI_PRO: 5.00,
36
+ }
37
+ OUTPUT_DOLLARS_PER_MILLION_TOKENS: ClassVar[dict[AIModel, float]] = {
38
+ AIModel.VERTEX_FLASH_8B: 0.30,
39
+ AIModel.VERTEX_FLASH: 0.15,
40
+ AIModel.VERTEX_PRO: 5.00,
41
+ AIModel.GEMINI_FLASH_8B: 0.30,
42
+ AIModel.GEMINI_FLASH: 0.15,
43
+ AIModel.GEMINI_PRO: 5.00,
44
+ }
45
+
46
+ response: R
47
+ run_id: str
48
+ flow_name: str
49
+ task_name: str
50
+ model: AIModel
51
+ system: LLMMessage | None = None
52
+ input_messages: list[LLMMessage]
53
+ input_tokens: int
54
+ output_tokens: int
55
+ start_time: datetime
56
+ end_time: datetime
57
+
58
+ @computed_field
59
+ @property
60
+ def duration_ms(self) -> int:
61
+ return int((self.end_time - self.start_time).total_seconds() * 1000)
62
+
63
+ @computed_field
64
+ @property
65
+ def input_cost(self) -> float:
66
+ return self.INPUT_DOLLARS_PER_MILLION_TOKENS[self.model] * self.input_tokens / 1_000_000
67
+
68
+ @computed_field
69
+ @property
70
+ def output_cost(self) -> float:
71
+ return self.OUTPUT_DOLLARS_PER_MILLION_TOKENS[self.model] * self.output_tokens / 1_000_000
72
+
73
+ @computed_field
74
+ @property
75
+ def total_cost(self) -> float:
76
+ return self.input_cost + self.output_cost
77
+
78
+ def minimized_dump(self) -> AgentResponseDump:
79
+ if self.system is None:
80
+ minimized_system_message = ""
81
+ else:
82
+ minimized_system_message = self.system
83
+ for part in minimized_system_message["content"]:
84
+ if part["type"] == "image_url":
85
+ part["image_url"] = "__MEDIA__"
86
+ minimized_system_message = json.dumps(minimized_system_message)
87
+
88
+ minimized_input_messages = [message for message in self.input_messages]
89
+ for message in minimized_input_messages:
90
+ for part in message["content"]:
91
+ if part["type"] == "image_url":
92
+ part["image_url"] = "__MEDIA__"
93
+ minimized_input_messages = [json.dumps(message) for message in minimized_input_messages]
94
+
95
+ output_message = self.response.model_dump_json() if isinstance(self.response, BaseModel) else self.response
96
+
97
+ return {
98
+ "run_id": self.run_id,
99
+ "flow_name": self.flow_name,
100
+ "task_name": self.task_name,
101
+ "model": self.model.value,
102
+ "system_message": minimized_system_message,
103
+ "input_messages": minimized_input_messages,
104
+ "output_message": output_message,
105
+ "input_tokens": self.input_tokens,
106
+ "output_tokens": self.output_tokens,
107
+ "input_cost": self.input_cost,
108
+ "output_cost": self.output_cost,
109
+ "total_cost": self.total_cost,
110
+ "start_time": self.start_time,
111
+ "end_time": self.end_time,
112
+ "duration_ms": self.duration_ms,
113
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "goose-py"
3
- version = "0.9.16"
3
+ version = "0.10.0"
4
4
  description = "A tool for AI workflows based on human-computer collaboration and structured output."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -24,6 +24,7 @@ packages = ["goose"]
24
24
 
25
25
  [dependency-groups]
26
26
  dev = [
27
+ "ipykernel>=6.29.5",
27
28
  "pyright>=1.1.393",
28
29
  "pytest>=8.3.4",
29
30
  "pytest-asyncio>=0.25.3",