ff-taskforce 0.1.0__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.
- ff_taskforce-0.1.0.dist-info/METADATA +136 -0
- ff_taskforce-0.1.0.dist-info/RECORD +38 -0
- ff_taskforce-0.1.0.dist-info/WHEEL +4 -0
- ff_taskforce-0.1.0.dist-info/licenses/LICENSE +21 -0
- taskforce/__init__.py +3 -0
- taskforce/for-agent-layerinfo.md +16 -0
- taskforce/l1/__init__.py +3 -0
- taskforce/l1/for-agent-layerinfo-l1.md +8 -0
- taskforce/l1/schema/__init__.py +3 -0
- taskforce/l1/schema/for-agent-moduleinfo.md +47 -0
- taskforce/l1/schema/schema.py +35 -0
- taskforce/l2/__init__.py +6 -0
- taskforce/l2/config/__init__.py +3 -0
- taskforce/l2/config/config.py +65 -0
- taskforce/l2/config/for-agent-moduleinfo.md +24 -0
- taskforce/l2/cost_logger/__init__.py +3 -0
- taskforce/l2/cost_logger/cost_logger.py +48 -0
- taskforce/l2/cost_logger/for-agent-moduleinfo.md +21 -0
- taskforce/l2/for-agent-layerinfo-l2.md +17 -0
- taskforce/l2/panelist/__init__.py +3 -0
- taskforce/l2/panelist/for-agent-moduleinfo.md +23 -0
- taskforce/l2/panelist/panelist.py +52 -0
- taskforce/l2/session_logger/__init__.py +3 -0
- taskforce/l2/session_logger/for-agent-moduleinfo.md +22 -0
- taskforce/l2/session_logger/session_logger.py +48 -0
- taskforce/l3/__init__.py +3 -0
- taskforce/l3/for-agent-layerinfo-l3.md +7 -0
- taskforce/l3/roundtable/__init__.py +3 -0
- taskforce/l3/roundtable/for-agent-moduleinfo.md +30 -0
- taskforce/l3/roundtable/roundtable.py +102 -0
- taskforce/l4/__init__.py +3 -0
- taskforce/l4/for-agent-layerinfo-l4.md +6 -0
- taskforce/l4/taskforce_facade/__init__.py +3 -0
- taskforce/l4/taskforce_facade/for-agent-moduleinfo.md +25 -0
- taskforce/l4/taskforce_facade/taskforce_facade.py +40 -0
- taskforce/mcp_wrapper/__init__.py +3 -0
- taskforce/mcp_wrapper/__main__.py +3 -0
- taskforce/mcp_wrapper/mcp_wrapper.py +48 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ff-taskforce
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-LLM roundtable for diverse perspective gathering
|
|
5
|
+
Project-URL: Repository, https://github.com/gatesplan/taskforce
|
|
6
|
+
Project-URL: Issues, https://github.com/gatesplan/taskforce/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent,llm,mcp,multi-model,roundtable
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: litellm
|
|
19
|
+
Requires-Dist: loguru
|
|
20
|
+
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Requires-Dist: python-dotenv
|
|
22
|
+
Provides-Extra: mcp
|
|
23
|
+
Requires-Dist: mcp; extra == 'mcp'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# taskforce
|
|
27
|
+
|
|
28
|
+
AI agent-oriented multi-LLM roundtable library.
|
|
29
|
+
|
|
30
|
+
Multiple top-tier LLMs (GPT, Grok, Claude, Gemini) are queried in parallel with the same agenda, and the collected opinions are classified into **common / divergent / unique** perspectives, returned as a structured `IdeaPool`.
|
|
31
|
+
|
|
32
|
+
**This package is designed for AI agents, not for direct human use.**
|
|
33
|
+
The primary interface is the MCP wrapper (`roundtable_discuss` tool), which allows agents to invoke a roundtable discussion as a tool call. A Python API is also available for programmatic integration.
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### 1. Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install taskforce
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For MCP server support:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install taskforce[mcp]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Set environment variables
|
|
50
|
+
|
|
51
|
+
At least two provider API keys are required (one will be excluded as the caller).
|
|
52
|
+
`XAI_API_KEY` is always required (used by the summarizer).
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
OPENAI_API_KEY=sk-...
|
|
56
|
+
XAI_API_KEY=xai-...
|
|
57
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
58
|
+
GEMINI_API_KEY=AI...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. Use as MCP tool (recommended for agents)
|
|
62
|
+
|
|
63
|
+
Add to your MCP server config:
|
|
64
|
+
|
|
65
|
+
`TASKFORCE_CALLER_PROVIDER` is the provider of the agent that will call this tool.
|
|
66
|
+
The matching provider's model is excluded from the panel -- querying the same model that is already reasoning adds no diversity.
|
|
67
|
+
For example, if Claude Code is the caller, set it to `"anthropic"` so Claude is excluded from the panel.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"taskforce": {
|
|
73
|
+
"command": "python",
|
|
74
|
+
"args": ["-m", "taskforce.mcp_wrapper"],
|
|
75
|
+
"env": {
|
|
76
|
+
"TASKFORCE_CALLER_PROVIDER": "anthropic"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The agent can then call the `roundtable_discuss` tool with `agenda` and `context` parameters.
|
|
84
|
+
|
|
85
|
+
### 4. Use as Python library
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from taskforce import Taskforce
|
|
89
|
+
|
|
90
|
+
tf = Taskforce(caller_provider="anthropic")
|
|
91
|
+
pool = tf.discuss(
|
|
92
|
+
agenda="Evaluate the trade-offs of approach A vs B",
|
|
93
|
+
context="<detailed context here>"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# pool.common -- list[str]: points most models agree on
|
|
97
|
+
# pool.divergent -- list[DivergentPoint]: topics with differing positions
|
|
98
|
+
# pool.unique -- list[UniquePoint]: points raised by only one model
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Important Notes
|
|
102
|
+
|
|
103
|
+
- **Paid API calls.** Every `discuss()` invocation calls multiple LLM APIs in parallel. Agents should confirm with the user before calling.
|
|
104
|
+
- **caller_provider exclusion.** The model from the same provider as the calling agent is excluded from the panel to maximize perspective diversity.
|
|
105
|
+
- **XAI_API_KEY is mandatory.** The summarizer (grok-4-1-fast-non-reasoning) always uses the XAI key.
|
|
106
|
+
- **Rich context matters.** Input tokens are cheap. Provide as much context as possible -- specifications, constraints, background, decisions already made -- so the panel can give concrete, actionable opinions instead of generic advice.
|
|
107
|
+
|
|
108
|
+
## API
|
|
109
|
+
|
|
110
|
+
### `Taskforce(caller_provider, dotenv_path=None)`
|
|
111
|
+
|
|
112
|
+
- `caller_provider` (`str`): The LLM provider of the calling agent (e.g. `"anthropic"`, `"openai"`). That provider's model is excluded from the panel.
|
|
113
|
+
- `dotenv_path` (`str | None`): Path to `.env` file. Defaults to auto-discovery.
|
|
114
|
+
|
|
115
|
+
### `Taskforce.discuss(agenda, context="") -> IdeaPool`
|
|
116
|
+
|
|
117
|
+
Synchronous wrapper. Queries the panel, summarizes, and returns an `IdeaPool`.
|
|
118
|
+
|
|
119
|
+
### `Taskforce.discuss_async(agenda, context="") -> IdeaPool`
|
|
120
|
+
|
|
121
|
+
Async version for use in async contexts.
|
|
122
|
+
|
|
123
|
+
### `IdeaPool`
|
|
124
|
+
|
|
125
|
+
| Field | Type | Description |
|
|
126
|
+
|-------|------|-------------|
|
|
127
|
+
| `agenda` | `str` | The original agenda |
|
|
128
|
+
| `common` | `list[str]` | Points most models agree on |
|
|
129
|
+
| `divergent` | `list[DivergentPoint]` | Topics with differing positions (`topic`, `positions: dict[model, position]`) |
|
|
130
|
+
| `unique` | `list[UniquePoint]` | Points from a single model (`point`, `source_model`) |
|
|
131
|
+
| `total_cost` | `float` | Total API cost (USD) |
|
|
132
|
+
| `total_tokens` | `int` | Total tokens consumed |
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
taskforce/__init__.py,sha256=RbWmYRJP0g3wcDAMLkT7eonFt2sJAOSPbVM6KQqf96g,68
|
|
2
|
+
taskforce/for-agent-layerinfo.md,sha256=UiwhcCYsbgFRDW0SnzjsqxFQqrsI9T9mvkHveibruqk,472
|
|
3
|
+
taskforce/l1/__init__.py,sha256=WPhPxdLAWkaxuHfzNpxI3vKIDlFa9Q6w5cALlfHlFJM,161
|
|
4
|
+
taskforce/l1/for-agent-layerinfo-l1.md,sha256=rUXo5NFVScE3gAKChddkgf8pjNg0FWHg32N2k7lFw3E,327
|
|
5
|
+
taskforce/l1/schema/__init__.py,sha256=WPhPxdLAWkaxuHfzNpxI3vKIDlFa9Q6w5cALlfHlFJM,161
|
|
6
|
+
taskforce/l1/schema/for-agent-moduleinfo.md,sha256=17kQf4-KSmd6FIei2PbtbKiqlBlbR4FZvDFopb51PmY,1122
|
|
7
|
+
taskforce/l1/schema/schema.py,sha256=BRnavvnZp3NA9-vXqxpTAFW1utBoJsd2HNKlwtkUP1g,608
|
|
8
|
+
taskforce/l2/__init__.py,sha256=-m0agG1snzxPrr8Rud9hS7m4msCjp2KrO_FmHBkUIUo,207
|
|
9
|
+
taskforce/l2/for-agent-layerinfo-l2.md,sha256=Y5QgI7C7Coz3N5t_tKurfJy4Pgy8mXreRQuTMrRyf-0,622
|
|
10
|
+
taskforce/l2/config/__init__.py,sha256=IMyl3II4ECLtsJ0CcDUYfR-h8pkWm1Zk7ChE-aX-aNU,55
|
|
11
|
+
taskforce/l2/config/config.py,sha256=W1igecQak7HiNOMk0T6Y07g6tDDypQ3ehWgIpsXhE_Y,2494
|
|
12
|
+
taskforce/l2/config/for-agent-moduleinfo.md,sha256=sBvvLzUEJQVXawgNSIlOOyFYs0Uq4b7OItbYFfasKF4,781
|
|
13
|
+
taskforce/l2/cost_logger/__init__.py,sha256=J2eqb0NP92XagSO7gIt5s3Jm_z46trkzYDRbzppU9QU,62
|
|
14
|
+
taskforce/l2/cost_logger/cost_logger.py,sha256=oHFrnrfxFDmFYyMX-WsMvmfGIoL_3IYKqCziStZN148,1549
|
|
15
|
+
taskforce/l2/cost_logger/for-agent-moduleinfo.md,sha256=ddRq_KVjbwJndkfeKluB6bfx3xnWhITc36FFRhOJZQA,595
|
|
16
|
+
taskforce/l2/panelist/__init__.py,sha256=NlGY0oet_C5AWApsCbkOaxvznDPwIOqKqvejKUT2M5U,55
|
|
17
|
+
taskforce/l2/panelist/for-agent-moduleinfo.md,sha256=_g7M5mTuqLkSlFqYbvJoB7wtR2IR_cNYOoZ0vh8rERU,582
|
|
18
|
+
taskforce/l2/panelist/panelist.py,sha256=rZWbcDHl6KAwTGh6ZD0R97T4g04ZjWXMbOg2ZBROTOk,1897
|
|
19
|
+
taskforce/l2/session_logger/__init__.py,sha256=i5R8g-8QNrQWshlGIqV91XumjZezfrZRYOJdmgtWuKQ,71
|
|
20
|
+
taskforce/l2/session_logger/for-agent-moduleinfo.md,sha256=l6_lgosPqp3y4beqds7U1L6S9hjGIQbhkuxcoK96GfE,611
|
|
21
|
+
taskforce/l2/session_logger/session_logger.py,sha256=PDIVd9WTNTqpvLq8QStPsyFFykPgVp7cL51-8Q-jtzc,1593
|
|
22
|
+
taskforce/l3/__init__.py,sha256=9KyOzm-QQS3MPFaP3sJQIJqUXNxfhTP5m_nLnsUn3VU,61
|
|
23
|
+
taskforce/l3/for-agent-layerinfo-l3.md,sha256=Vuo2ofbPbVRMKzLGwBskgZ5ckdta2mi2YRgzHbOnvIg,290
|
|
24
|
+
taskforce/l3/roundtable/__init__.py,sha256=9KyOzm-QQS3MPFaP3sJQIJqUXNxfhTP5m_nLnsUn3VU,61
|
|
25
|
+
taskforce/l3/roundtable/for-agent-moduleinfo.md,sha256=AALwBRRZc6_nvejn8XV24N4DkRmvLF_tbu-sLS7yJaI,885
|
|
26
|
+
taskforce/l3/roundtable/roundtable.py,sha256=DOb8aysINeQS4bp_21LW9CQAYP4GBeO-1Win-Fde2fA,3982
|
|
27
|
+
taskforce/l4/__init__.py,sha256=EnxeuegsZP8DT8D0Ziy36zbVvpeTUUKjP2BfAMzwSV4,65
|
|
28
|
+
taskforce/l4/for-agent-layerinfo-l4.md,sha256=ynFygOe89Z06cNLwOkoztW1AEr7rFUq6a_azmNAo2Mo,229
|
|
29
|
+
taskforce/l4/taskforce_facade/__init__.py,sha256=EnxeuegsZP8DT8D0Ziy36zbVvpeTUUKjP2BfAMzwSV4,65
|
|
30
|
+
taskforce/l4/taskforce_facade/for-agent-moduleinfo.md,sha256=mju3fdECZN0m0hcoIpklP37DeZlV2wkIVROovNLGIoY,711
|
|
31
|
+
taskforce/l4/taskforce_facade/taskforce_facade.py,sha256=6LlSMwa30NPLaknSCXuiaDM4-sTrc752P4xgs8OwemQ,1437
|
|
32
|
+
taskforce/mcp_wrapper/__init__.py,sha256=ethK8wvFtEwoanJ7QvREYfFkm2430FtU5SPuipmEGOw,48
|
|
33
|
+
taskforce/mcp_wrapper/__main__.py,sha256=U1mIekzzLhZIprEhZ7yLp_ysVnYjukAE-XLJe_8KBPg,40
|
|
34
|
+
taskforce/mcp_wrapper/mcp_wrapper.py,sha256=_UWYQwswwMi8n0RVrAcCS06yJJ6KfheyMQpr1cKdxoE,1875
|
|
35
|
+
ff_taskforce-0.1.0.dist-info/METADATA,sha256=zixX-N7SUyozOI1OMMpckDGR72vrZsgxJdVkGUI4WGQ,4712
|
|
36
|
+
ff_taskforce-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
37
|
+
ff_taskforce-0.1.0.dist-info/licenses/LICENSE,sha256=Is6uQxKak3NHGPiVNoiMJUOEmLcvt5yqetu2h3RYTlI,1079
|
|
38
|
+
ff_taskforce-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 taskforce contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
taskforce/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# taskforce
|
|
2
|
+
|
|
3
|
+
## l1
|
|
4
|
+
- schema: 데이터 모델 (ModelEntry, Opinion, IdeaPool)
|
|
5
|
+
|
|
6
|
+
## l2
|
|
7
|
+
- config: .env 로드, 모델 레지스트리, caller 제외 로직
|
|
8
|
+
- panelist: LiteLLM 래퍼, 개별 모델 async 호출
|
|
9
|
+
- session_logger: 세션 전문 로그 (~/.taskforce/logs/)
|
|
10
|
+
- cost_logger: 비용 요약 로그 (~/.taskforce/cost_logs/)
|
|
11
|
+
|
|
12
|
+
## l3
|
|
13
|
+
- roundtable: 병렬 질의 + summarizer 분류 취합
|
|
14
|
+
|
|
15
|
+
## l4
|
|
16
|
+
- taskforce_facade: 최상위 퍼사드, 초기화/discuss/로그 기록
|
taskforce/l1/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# l1
|
|
2
|
+
|
|
3
|
+
## schema
|
|
4
|
+
ModelEntry(provider: str, model_id: str, api_key: str, display_name: str)
|
|
5
|
+
Opinion(model_name: str, content: str)
|
|
6
|
+
DivergentPoint(topic: str, positions: dict[str, str])
|
|
7
|
+
UniquePoint(point: str, source_model: str)
|
|
8
|
+
IdeaPool(agenda: str, common: list[str], divergent: list[DivergentPoint], unique: list[UniquePoint])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Schema
|
|
2
|
+
|
|
3
|
+
pydantic 데이터 모델 정의.
|
|
4
|
+
|
|
5
|
+
## ModelEntry
|
|
6
|
+
|
|
7
|
+
LLM 프로바이더 모델 정보.
|
|
8
|
+
|
|
9
|
+
### Properties
|
|
10
|
+
provider: str # 프로바이더명 (openai, xai, anthropic, gemini)
|
|
11
|
+
model_id: str # LiteLLM 모델 ID
|
|
12
|
+
api_key: str # API 키
|
|
13
|
+
display_name: str # 표시용 이름
|
|
14
|
+
|
|
15
|
+
## Opinion
|
|
16
|
+
|
|
17
|
+
패널리스트 응답.
|
|
18
|
+
|
|
19
|
+
### Properties
|
|
20
|
+
model_name: str # 응답한 모델 표시명
|
|
21
|
+
content: str # 응답 전문
|
|
22
|
+
|
|
23
|
+
## DivergentPoint
|
|
24
|
+
|
|
25
|
+
모델 간 의견이 갈리는 지점.
|
|
26
|
+
|
|
27
|
+
### Properties
|
|
28
|
+
topic: str # 쟁점
|
|
29
|
+
positions: dict[str, str] # {model_name: 해당 입장}
|
|
30
|
+
|
|
31
|
+
## UniquePoint
|
|
32
|
+
|
|
33
|
+
한 모델만 제시한 고유 관점.
|
|
34
|
+
|
|
35
|
+
### Properties
|
|
36
|
+
point: str # 고유 관점 내용
|
|
37
|
+
source_model: str # 제시한 모델명
|
|
38
|
+
|
|
39
|
+
## IdeaPool
|
|
40
|
+
|
|
41
|
+
라운드테이블 결과. 구조화된 아이디어 풀.
|
|
42
|
+
|
|
43
|
+
### Properties
|
|
44
|
+
agenda: str # 원본 의제
|
|
45
|
+
common: list[str] # 다수 모델 공통 포인트
|
|
46
|
+
divergent: list[DivergentPoint] # 이견 지점
|
|
47
|
+
unique: list[UniquePoint] # 고유 관점
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ModelEntry(BaseModel):
|
|
5
|
+
provider: str
|
|
6
|
+
model_id: str
|
|
7
|
+
api_key: str
|
|
8
|
+
display_name: str
|
|
9
|
+
extra_params: dict = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Opinion(BaseModel):
|
|
13
|
+
model_name: str
|
|
14
|
+
content: str
|
|
15
|
+
cost: float = 0.0
|
|
16
|
+
tokens: int = 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DivergentPoint(BaseModel):
|
|
20
|
+
topic: str
|
|
21
|
+
positions: dict[str, str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UniquePoint(BaseModel):
|
|
25
|
+
point: str
|
|
26
|
+
source_model: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class IdeaPool(BaseModel):
|
|
30
|
+
agenda: str
|
|
31
|
+
common: list[str]
|
|
32
|
+
divergent: list[DivergentPoint]
|
|
33
|
+
unique: list[UniquePoint]
|
|
34
|
+
total_cost: float = 0.0
|
|
35
|
+
total_tokens: int = 0
|
taskforce/l2/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from ...l1.schema import ModelEntry
|
|
7
|
+
|
|
8
|
+
# 환경변수 -> 패널 모델 매핑
|
|
9
|
+
_PANEL_REGISTRY = [
|
|
10
|
+
{"env_key": "OPENAI_API_KEY", "provider": "openai", "model_id": "gpt-5.4", "display_name": "GPT 5.4"},
|
|
11
|
+
{"env_key": "XAI_API_KEY", "provider": "xai", "model_id": "grok-4-1-fast-reasoning", "display_name": "Grok 4-1"},
|
|
12
|
+
{"env_key": "ANTHROPIC_API_KEY", "provider": "anthropic", "model_id": "claude-opus-4", "display_name": "Claude opus-4"},
|
|
13
|
+
{"env_key": "GEMINI_API_KEY", "provider": "gemini", "model_id": "gemini-2.5-pro", "display_name": "Gemini 2.5 Pro"},
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
_SUMMARIZER_MODEL_ID = "grok-4-1-fast-non-reasoning"
|
|
17
|
+
_SUMMARIZER_DISPLAY_NAME = "Grok 4-1 Fast"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EnvConfig:
|
|
21
|
+
def __init__(self, caller_provider: str, dotenv_path: str | None = None):
|
|
22
|
+
self.caller_provider = caller_provider
|
|
23
|
+
|
|
24
|
+
if dotenv_path:
|
|
25
|
+
load_dotenv(dotenv_path)
|
|
26
|
+
else:
|
|
27
|
+
load_dotenv()
|
|
28
|
+
|
|
29
|
+
# caller_provider 제외, 키가 있는 모델만 패널 등록
|
|
30
|
+
self.panel_models: list[ModelEntry] = []
|
|
31
|
+
for entry in _PANEL_REGISTRY:
|
|
32
|
+
api_key = os.getenv(entry["env_key"], "")
|
|
33
|
+
if not api_key:
|
|
34
|
+
continue
|
|
35
|
+
if entry["provider"] == caller_provider:
|
|
36
|
+
logger.debug(f"패널 제외 (caller): {entry['display_name']}")
|
|
37
|
+
continue
|
|
38
|
+
self.panel_models.append(ModelEntry(
|
|
39
|
+
provider=entry["provider"],
|
|
40
|
+
model_id=entry["model_id"],
|
|
41
|
+
api_key=api_key,
|
|
42
|
+
display_name=entry["display_name"],
|
|
43
|
+
extra_params=entry.get("extra_params", {}),
|
|
44
|
+
))
|
|
45
|
+
logger.debug(f"패널 등록: {entry['display_name']}")
|
|
46
|
+
|
|
47
|
+
if not self.panel_models:
|
|
48
|
+
raise ValueError("활성 패널 모델이 없습니다. .env에 API 키를 설정하세요.")
|
|
49
|
+
|
|
50
|
+
# Summarizer (XAI_API_KEY 공유)
|
|
51
|
+
xai_key = os.getenv("XAI_API_KEY", "")
|
|
52
|
+
if not xai_key:
|
|
53
|
+
raise ValueError("XAI_API_KEY 필요 (summarizer용)")
|
|
54
|
+
|
|
55
|
+
self.summarizer_model = ModelEntry(
|
|
56
|
+
provider="xai",
|
|
57
|
+
model_id=_SUMMARIZER_MODEL_ID,
|
|
58
|
+
api_key=xai_key,
|
|
59
|
+
display_name=_SUMMARIZER_DISPLAY_NAME,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
logger.info(f"EnvConfig 초기화: 패널 {len(self.panel_models)}개, caller={caller_provider}")
|
|
63
|
+
|
|
64
|
+
def get_active_panels(self) -> list[ModelEntry]:
|
|
65
|
+
return self.panel_models
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Config
|
|
2
|
+
|
|
3
|
+
.env에서 API 키를 로드하고 모델 레지스트리를 구성.
|
|
4
|
+
|
|
5
|
+
## EnvConfig
|
|
6
|
+
|
|
7
|
+
환경변수 기반 모델 구성. caller_provider 모델은 패널에서 제외.
|
|
8
|
+
|
|
9
|
+
### Properties
|
|
10
|
+
caller_provider: str # 호출자 프로바이더
|
|
11
|
+
panel_models: list[ModelEntry] # 활성 패널 모델 목록
|
|
12
|
+
summarizer_model: ModelEntry # Summarizer 모델 (grok-4-1-fast-non-reasoning)
|
|
13
|
+
|
|
14
|
+
### __init__
|
|
15
|
+
__init__(caller_provider: str, dotenv_path: str | None = None)
|
|
16
|
+
raise ValueError
|
|
17
|
+
.env 로드 후 키가 있고 caller가 아닌 모델만 패널 등록.
|
|
18
|
+
활성 패널 0개이면 ValueError.
|
|
19
|
+
XAI_API_KEY 없으면 ValueError (summarizer 필수).
|
|
20
|
+
|
|
21
|
+
### Methods
|
|
22
|
+
|
|
23
|
+
get_active_panels() -> list[ModelEntry]
|
|
24
|
+
등록된 패널 모델 목록 반환.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from ...l1.schema import Opinion
|
|
8
|
+
|
|
9
|
+
_DEFAULT_DIR = Path.home() / ".taskforce" / "cost_logs"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CostLogger:
|
|
13
|
+
def __init__(self, log_dir: Path | None = None):
|
|
14
|
+
self.log_dir = log_dir or _DEFAULT_DIR
|
|
15
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
def log(self, agenda: str, opinions: list[Opinion], summarizer_cost: float = 0.0, summarizer_tokens: int = 0) -> None:
|
|
18
|
+
timestamp = datetime.now().isoformat()
|
|
19
|
+
|
|
20
|
+
models = []
|
|
21
|
+
for op in opinions:
|
|
22
|
+
models.append({
|
|
23
|
+
"model_name": op.model_name,
|
|
24
|
+
"tokens": op.tokens,
|
|
25
|
+
"cost": op.cost,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
total_cost = sum(op.cost for op in opinions) + summarizer_cost
|
|
29
|
+
total_tokens = sum(op.tokens for op in opinions) + summarizer_tokens
|
|
30
|
+
|
|
31
|
+
record = {
|
|
32
|
+
"timestamp": timestamp,
|
|
33
|
+
"agenda": agenda[:100],
|
|
34
|
+
"models": models,
|
|
35
|
+
"summarizer_cost": summarizer_cost,
|
|
36
|
+
"summarizer_tokens": summarizer_tokens,
|
|
37
|
+
"total_cost": total_cost,
|
|
38
|
+
"total_tokens": total_tokens,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 월별 파일로 분리
|
|
42
|
+
month = datetime.now().strftime("%Y-%m")
|
|
43
|
+
filepath = self.log_dir / f"{month}.jsonl"
|
|
44
|
+
|
|
45
|
+
with open(filepath, "a", encoding="utf-8") as f:
|
|
46
|
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
47
|
+
|
|
48
|
+
logger.debug(f"비용 로그 기록: ${total_cost:.4f}, {total_tokens} tokens")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# CostLogger
|
|
2
|
+
|
|
3
|
+
비용 요약 로그 기록. 모델별 tokens/cost를 JSONL로 저장.
|
|
4
|
+
|
|
5
|
+
## CostLogger
|
|
6
|
+
|
|
7
|
+
~/.taskforce/cost_logs/ 에 월별 JSONL 파일로 누적 기록.
|
|
8
|
+
|
|
9
|
+
### Properties
|
|
10
|
+
log_dir: Path # 로그 디렉토리 경로
|
|
11
|
+
|
|
12
|
+
### __init__
|
|
13
|
+
__init__(log_dir: Path | None = None)
|
|
14
|
+
log_dir 미지정 시 ~/.taskforce/cost_logs/ 사용.
|
|
15
|
+
디렉토리 자동 생성.
|
|
16
|
+
|
|
17
|
+
### Methods
|
|
18
|
+
|
|
19
|
+
log(agenda: str, opinions: list[Opinion], summarizer_cost: float = 0.0, summarizer_tokens: int = 0) -> None
|
|
20
|
+
비용 레코드를 월별 파일에 append.
|
|
21
|
+
모델별 tokens/cost + 총계 기록.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# l2
|
|
2
|
+
|
|
3
|
+
## config
|
|
4
|
+
EnvConfig.__init__(caller_provider: str, dotenv_path: str | None = None)
|
|
5
|
+
EnvConfig.get_active_panels() -> list[ModelEntry]
|
|
6
|
+
|
|
7
|
+
## panelist
|
|
8
|
+
Panelist.__init__(model_entry: ModelEntry)
|
|
9
|
+
Panelist.ask_async(prompt: str, system_msg: str = "") -> Opinion
|
|
10
|
+
|
|
11
|
+
## session_logger
|
|
12
|
+
SessionLogger.__init__(log_dir: Path | None = None)
|
|
13
|
+
SessionLogger.log(agenda: str, context: str, opinions: list[Opinion], summarizer_output: str = "") -> Path
|
|
14
|
+
|
|
15
|
+
## cost_logger
|
|
16
|
+
CostLogger.__init__(log_dir: Path | None = None)
|
|
17
|
+
CostLogger.log(agenda: str, opinions: list[Opinion], summarizer_cost: float = 0.0, summarizer_tokens: int = 0) -> None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Panelist
|
|
2
|
+
|
|
3
|
+
LiteLLM 래퍼. 개별 LLM 모델에 async 질의.
|
|
4
|
+
|
|
5
|
+
## Panelist
|
|
6
|
+
|
|
7
|
+
ModelEntry를 받아 LiteLLM acompletion으로 호출.
|
|
8
|
+
|
|
9
|
+
### Properties
|
|
10
|
+
model_entry: ModelEntry # 모델 정보
|
|
11
|
+
|
|
12
|
+
### __init__
|
|
13
|
+
__init__(model_entry: ModelEntry)
|
|
14
|
+
ModelEntry 기반으로 LiteLLM 모델 ID 해석.
|
|
15
|
+
OpenAI는 model_id만, 나머지는 provider/model_id 형식.
|
|
16
|
+
|
|
17
|
+
### Methods
|
|
18
|
+
|
|
19
|
+
ask_async(prompt: str, system_msg: str = "") -> Opinion
|
|
20
|
+
raise Exception
|
|
21
|
+
LiteLLM acompletion으로 모델 호출.
|
|
22
|
+
system_msg가 있으면 system role로 전달.
|
|
23
|
+
실패 시 원본 예외 전파.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import litellm
|
|
2
|
+
from loguru import logger
|
|
3
|
+
|
|
4
|
+
from ...l1.schema import ModelEntry, Opinion
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Panelist:
|
|
8
|
+
def __init__(self, model_entry: ModelEntry):
|
|
9
|
+
self.model_entry = model_entry
|
|
10
|
+
self._litellm_model = self._resolve_model_id()
|
|
11
|
+
|
|
12
|
+
def _resolve_model_id(self) -> str:
|
|
13
|
+
# LiteLLM: OpenAI는 model_id만, 나머지는 provider/model_id
|
|
14
|
+
if self.model_entry.provider == "openai":
|
|
15
|
+
return self.model_entry.model_id
|
|
16
|
+
return f"{self.model_entry.provider}/{self.model_entry.model_id}"
|
|
17
|
+
|
|
18
|
+
async def ask_async(self, prompt: str, system_msg: str = "") -> Opinion:
|
|
19
|
+
messages = []
|
|
20
|
+
if system_msg:
|
|
21
|
+
messages.append({"role": "system", "content": system_msg})
|
|
22
|
+
messages.append({"role": "user", "content": prompt})
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
response = await litellm.acompletion(
|
|
26
|
+
model=self._litellm_model,
|
|
27
|
+
messages=messages,
|
|
28
|
+
api_key=self.model_entry.api_key,
|
|
29
|
+
**self.model_entry.extra_params,
|
|
30
|
+
)
|
|
31
|
+
content = response.choices[0].message.content
|
|
32
|
+
|
|
33
|
+
# 비용/토큰 추출
|
|
34
|
+
cost = 0.0
|
|
35
|
+
tokens = 0
|
|
36
|
+
try:
|
|
37
|
+
cost = litellm.completion_cost(completion_response=response)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
if hasattr(response, "usage") and response.usage:
|
|
41
|
+
tokens = getattr(response.usage, "total_tokens", 0)
|
|
42
|
+
|
|
43
|
+
logger.debug(f"{self.model_entry.display_name} 응답: {len(content)} chars, {tokens} tokens, ${cost:.4f}")
|
|
44
|
+
return Opinion(
|
|
45
|
+
model_name=self.model_entry.display_name,
|
|
46
|
+
content=content,
|
|
47
|
+
cost=cost,
|
|
48
|
+
tokens=tokens,
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"{self.model_entry.display_name} 호출 실패: {e}")
|
|
52
|
+
raise
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SessionLogger
|
|
2
|
+
|
|
3
|
+
세션 전문 로그 기록. prompt, response 원문을 JSONL로 저장.
|
|
4
|
+
|
|
5
|
+
## SessionLogger
|
|
6
|
+
|
|
7
|
+
~/.taskforce/logs/ 에 세션별 JSONL 파일 생성.
|
|
8
|
+
|
|
9
|
+
### Properties
|
|
10
|
+
log_dir: Path # 로그 디렉토리 경로
|
|
11
|
+
|
|
12
|
+
### __init__
|
|
13
|
+
__init__(log_dir: Path | None = None)
|
|
14
|
+
log_dir 미지정 시 ~/.taskforce/logs/ 사용.
|
|
15
|
+
디렉토리 자동 생성.
|
|
16
|
+
|
|
17
|
+
### Methods
|
|
18
|
+
|
|
19
|
+
log(agenda: str, context: str, opinions: list[Opinion], summarizer_output: str = "") -> Path
|
|
20
|
+
세션 로그 파일 생성.
|
|
21
|
+
request, opinion, summarizer 레코드를 JSONL로 기록.
|
|
22
|
+
생성된 파일 경로 반환.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from ...l1.schema import Opinion
|
|
8
|
+
|
|
9
|
+
_DEFAULT_DIR = Path.home() / ".taskforce" / "logs"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SessionLogger:
|
|
13
|
+
def __init__(self, log_dir: Path | None = None):
|
|
14
|
+
self.log_dir = log_dir or _DEFAULT_DIR
|
|
15
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
def log(self, agenda: str, context: str, opinions: list[Opinion], summarizer_output: str = "") -> Path:
|
|
18
|
+
timestamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
|
|
19
|
+
filepath = self.log_dir / f"{timestamp}.jsonl"
|
|
20
|
+
|
|
21
|
+
with open(filepath, "a", encoding="utf-8") as f:
|
|
22
|
+
# 의제 기록
|
|
23
|
+
f.write(json.dumps({
|
|
24
|
+
"type": "request",
|
|
25
|
+
"timestamp": timestamp,
|
|
26
|
+
"agenda": agenda,
|
|
27
|
+
"context": context,
|
|
28
|
+
}, ensure_ascii=False) + "\n")
|
|
29
|
+
|
|
30
|
+
# 각 패널 응답 기록
|
|
31
|
+
for op in opinions:
|
|
32
|
+
f.write(json.dumps({
|
|
33
|
+
"type": "opinion",
|
|
34
|
+
"model_name": op.model_name,
|
|
35
|
+
"content": op.content,
|
|
36
|
+
"tokens": op.tokens,
|
|
37
|
+
"cost": op.cost,
|
|
38
|
+
}, ensure_ascii=False) + "\n")
|
|
39
|
+
|
|
40
|
+
# summarizer 출력 기록
|
|
41
|
+
if summarizer_output:
|
|
42
|
+
f.write(json.dumps({
|
|
43
|
+
"type": "summarizer",
|
|
44
|
+
"content": summarizer_output,
|
|
45
|
+
}, ensure_ascii=False) + "\n")
|
|
46
|
+
|
|
47
|
+
logger.debug(f"세션 로그 기록: {filepath}")
|
|
48
|
+
return filepath
|
taskforce/l3/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# l3
|
|
2
|
+
|
|
3
|
+
## roundtable
|
|
4
|
+
Roundtable.__init__(panelists: list[Panelist], summarizer: Panelist)
|
|
5
|
+
Roundtable.gather(agenda: str, context: str = "") -> list[Opinion]
|
|
6
|
+
Roundtable.summarize(agenda: str, opinions: list[Opinion]) -> IdeaPool
|
|
7
|
+
Roundtable.discuss(agenda: str, context: str = "") -> IdeaPool
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Roundtable
|
|
2
|
+
|
|
3
|
+
병렬 패널 질의 + summarizer 분류 취합.
|
|
4
|
+
|
|
5
|
+
## Roundtable
|
|
6
|
+
|
|
7
|
+
다수 패널리스트에 의제를 병렬 질의하고 summarizer로 분류.
|
|
8
|
+
|
|
9
|
+
### Properties
|
|
10
|
+
panelists: list[Panelist] # 패널 모델 목록
|
|
11
|
+
summarizer: Panelist # 분류 취합용 모델
|
|
12
|
+
|
|
13
|
+
### __init__
|
|
14
|
+
__init__(panelists: list[Panelist], summarizer: Panelist)
|
|
15
|
+
패널과 summarizer 설정.
|
|
16
|
+
|
|
17
|
+
### Methods
|
|
18
|
+
|
|
19
|
+
gather(agenda: str, context: str = "") -> list[Opinion]
|
|
20
|
+
asyncio.gather로 전체 패널 병렬 질의.
|
|
21
|
+
실패한 패널은 제외, 성공분만 반환.
|
|
22
|
+
|
|
23
|
+
summarize(agenda: str, opinions: list[Opinion]) -> IdeaPool
|
|
24
|
+
raise ValueError
|
|
25
|
+
opinions를 summarizer에 전달하여 common/divergent/unique 분류.
|
|
26
|
+
JSON 파싱 실패 시 ValueError.
|
|
27
|
+
|
|
28
|
+
discuss(agenda: str, context: str = "") -> IdeaPool
|
|
29
|
+
gather + summarize 일괄 실행.
|
|
30
|
+
의견 0개이면 빈 IdeaPool 반환.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from ...l1.schema import Opinion, IdeaPool, DivergentPoint, UniquePoint
|
|
8
|
+
from ...l2.panelist import Panelist
|
|
9
|
+
|
|
10
|
+
_PANEL_SYSTEM_MSG = (
|
|
11
|
+
"You are an expert providing your independent analysis. "
|
|
12
|
+
"Be specific, thorough, and present your unique perspective."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_SUMMARIZER_SYSTEM_MSG = (
|
|
16
|
+
"You are an analyst. Given multiple expert opinions on an agenda, "
|
|
17
|
+
"classify the key points into three categories.\n"
|
|
18
|
+
"Respond ONLY in valid JSON with this exact structure:\n"
|
|
19
|
+
"{\n"
|
|
20
|
+
' "common": ["point shared by most experts", ...],\n'
|
|
21
|
+
' "divergent": [{"topic": "topic", "positions": {"Expert1": "position", "Expert2": "position"}}, ...],\n'
|
|
22
|
+
' "unique": [{"point": "insight", "source_model": "ExpertName"}, ...]\n'
|
|
23
|
+
"}\n"
|
|
24
|
+
"Rules:\n"
|
|
25
|
+
"- common: points that 2+ experts agree on\n"
|
|
26
|
+
"- divergent: topics where experts have clearly different positions\n"
|
|
27
|
+
"- unique: points raised by only one expert\n"
|
|
28
|
+
"- Be thorough, do not lose information\n"
|
|
29
|
+
"- Respond in the same language as the opinions"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Roundtable:
|
|
34
|
+
def __init__(self, panelists: list[Panelist], summarizer: Panelist):
|
|
35
|
+
self.panelists = panelists
|
|
36
|
+
self.summarizer = summarizer
|
|
37
|
+
logger.info(f"Roundtable 구성: 패널 {len(panelists)}명")
|
|
38
|
+
|
|
39
|
+
async def gather(self, agenda: str, context: str = "") -> list[Opinion]:
|
|
40
|
+
prompt = self._build_prompt(agenda, context)
|
|
41
|
+
tasks = [p.ask_async(prompt, _PANEL_SYSTEM_MSG) for p in self.panelists]
|
|
42
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
43
|
+
|
|
44
|
+
opinions = []
|
|
45
|
+
for i, result in enumerate(results):
|
|
46
|
+
if isinstance(result, Exception):
|
|
47
|
+
name = self.panelists[i].model_entry.display_name
|
|
48
|
+
logger.warning(f"패널 {name} 실패, 제외: {result}")
|
|
49
|
+
continue
|
|
50
|
+
opinions.append(result)
|
|
51
|
+
|
|
52
|
+
logger.info(f"의견 수집: {len(opinions)}/{len(self.panelists)}")
|
|
53
|
+
return opinions
|
|
54
|
+
|
|
55
|
+
async def summarize(self, agenda: str, opinions: list[Opinion]) -> IdeaPool:
|
|
56
|
+
opinions_text = "\n\n".join(
|
|
57
|
+
f"--- {op.model_name} ---\n{op.content}" for op in opinions
|
|
58
|
+
)
|
|
59
|
+
prompt = f"Agenda: {agenda}\n\nExpert Opinions:\n{opinions_text}"
|
|
60
|
+
|
|
61
|
+
result = await self.summarizer.ask_async(prompt, _SUMMARIZER_SYSTEM_MSG)
|
|
62
|
+
parsed = self._extract_json(result.content)
|
|
63
|
+
|
|
64
|
+
# 비용 합산: 패널 + summarizer
|
|
65
|
+
total_cost = sum(op.cost for op in opinions) + result.cost
|
|
66
|
+
total_tokens = sum(op.tokens for op in opinions) + result.tokens
|
|
67
|
+
|
|
68
|
+
return IdeaPool(
|
|
69
|
+
agenda=agenda,
|
|
70
|
+
common=parsed.get("common", []),
|
|
71
|
+
divergent=[DivergentPoint(**d) for d in parsed.get("divergent", [])],
|
|
72
|
+
unique=[UniquePoint(**u) for u in parsed.get("unique", [])],
|
|
73
|
+
total_cost=total_cost,
|
|
74
|
+
total_tokens=total_tokens,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def discuss(self, agenda: str, context: str = "") -> tuple[IdeaPool, list[Opinion]]:
|
|
78
|
+
opinions = await self.gather(agenda, context)
|
|
79
|
+
if not opinions:
|
|
80
|
+
logger.warning("수집된 의견 없음")
|
|
81
|
+
pool = IdeaPool(agenda=agenda, common=[], divergent=[], unique=[])
|
|
82
|
+
return pool, []
|
|
83
|
+
pool = await self.summarize(agenda, opinions)
|
|
84
|
+
return pool, opinions
|
|
85
|
+
|
|
86
|
+
def _build_prompt(self, agenda: str, context: str = "") -> str:
|
|
87
|
+
parts = [f"Agenda: {agenda}"]
|
|
88
|
+
if context:
|
|
89
|
+
parts.insert(0, f"Context: {context}")
|
|
90
|
+
return "\n\n".join(parts)
|
|
91
|
+
|
|
92
|
+
def _extract_json(self, text: str) -> dict:
|
|
93
|
+
match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
|
|
94
|
+
if match:
|
|
95
|
+
text = match.group(1)
|
|
96
|
+
|
|
97
|
+
start = text.find("{")
|
|
98
|
+
end = text.rfind("}")
|
|
99
|
+
if start == -1 or end == -1:
|
|
100
|
+
raise ValueError(f"JSON 파싱 실패: {text[:200]}")
|
|
101
|
+
|
|
102
|
+
return json.loads(text[start:end + 1])
|
taskforce/l4/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Taskforce Facade
|
|
2
|
+
|
|
3
|
+
최상위 진입점. 초기화와 discuss 호출을 단순화.
|
|
4
|
+
|
|
5
|
+
## Taskforce
|
|
6
|
+
|
|
7
|
+
EnvConfig + Roundtable 조합을 단일 인터페이스로 노출.
|
|
8
|
+
|
|
9
|
+
### Properties
|
|
10
|
+
config: EnvConfig # 환경 설정
|
|
11
|
+
roundtable: Roundtable # 라운드테이블 인스턴스
|
|
12
|
+
|
|
13
|
+
### __init__
|
|
14
|
+
__init__(caller_provider: str, dotenv_path: str | None = None)
|
|
15
|
+
raise ValueError
|
|
16
|
+
EnvConfig 로드, Panelist/Roundtable 자동 구성.
|
|
17
|
+
EnvConfig 실패 시 ValueError 전파.
|
|
18
|
+
|
|
19
|
+
### Methods
|
|
20
|
+
|
|
21
|
+
discuss_async(agenda: str, context: str = "") -> IdeaPool
|
|
22
|
+
async 버전. roundtable.discuss 위임.
|
|
23
|
+
|
|
24
|
+
discuss(agenda: str, context: str = "") -> IdeaPool
|
|
25
|
+
sync 버전. asyncio.run으로 discuss_async 실행.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from ...l1.schema import IdeaPool
|
|
6
|
+
from ...l2.config import EnvConfig
|
|
7
|
+
from ...l2.panelist import Panelist
|
|
8
|
+
from ...l2.session_logger import SessionLogger
|
|
9
|
+
from ...l2.cost_logger import CostLogger
|
|
10
|
+
from ...l3.roundtable import Roundtable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Taskforce:
|
|
14
|
+
def __init__(self, caller_provider: str, dotenv_path: str | None = None):
|
|
15
|
+
self.config = EnvConfig(caller_provider, dotenv_path)
|
|
16
|
+
|
|
17
|
+
panelists = [Panelist(m) for m in self.config.get_active_panels()]
|
|
18
|
+
summarizer = Panelist(self.config.summarizer_model)
|
|
19
|
+
self.roundtable = Roundtable(panelists, summarizer)
|
|
20
|
+
|
|
21
|
+
self.session_logger = SessionLogger()
|
|
22
|
+
self.cost_logger = CostLogger()
|
|
23
|
+
|
|
24
|
+
logger.info("Taskforce 초기화 완료")
|
|
25
|
+
|
|
26
|
+
async def discuss_async(self, agenda: str, context: str = "") -> IdeaPool:
|
|
27
|
+
pool, opinions = await self.roundtable.discuss(agenda, context)
|
|
28
|
+
|
|
29
|
+
# 로그 기록
|
|
30
|
+
if opinions:
|
|
31
|
+
summarizer_cost = pool.total_cost - sum(op.cost for op in opinions)
|
|
32
|
+
summarizer_tokens = pool.total_tokens - sum(op.tokens for op in opinions)
|
|
33
|
+
|
|
34
|
+
self.session_logger.log(agenda, context, opinions)
|
|
35
|
+
self.cost_logger.log(agenda, opinions, summarizer_cost, summarizer_tokens)
|
|
36
|
+
|
|
37
|
+
return pool
|
|
38
|
+
|
|
39
|
+
def discuss(self, agenda: str, context: str = "") -> IdeaPool:
|
|
40
|
+
return asyncio.run(self.discuss_async(agenda, context))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from mcp.server.fastmcp import FastMCP
|
|
4
|
+
|
|
5
|
+
from ..l4.taskforce_facade import Taskforce
|
|
6
|
+
|
|
7
|
+
mcp = FastMCP("taskforce")
|
|
8
|
+
|
|
9
|
+
_tf = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_taskforce() -> Taskforce:
|
|
13
|
+
global _tf
|
|
14
|
+
if _tf is None:
|
|
15
|
+
caller = os.getenv("TASKFORCE_CALLER_PROVIDER", "anthropic")
|
|
16
|
+
_tf = Taskforce(caller_provider=caller)
|
|
17
|
+
return _tf
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
async def roundtable_discuss(agenda: str, context: str = "") -> str:
|
|
22
|
+
"""Roundtable: 다수의 최고 성능 LLM(GPT, Grok, Claude, Gemini)에
|
|
23
|
+
동일 의제를 독립적으로 질의하고, 수집된 의견을 common/divergent/unique로
|
|
24
|
+
분류하여 구조화된 JSON을 반환한다.
|
|
25
|
+
|
|
26
|
+
유료 API를 호출하는 도구이다. 사용 전 반드시 사용자에게 의견을 묻고,
|
|
27
|
+
사용자가 동의한 경우에만 호출하라.
|
|
28
|
+
|
|
29
|
+
서로 다른 방식으로 발달한 모델들의 관점 다양성을 활용하여
|
|
30
|
+
robust한 아이디어 풀을 구축하는 것이 목적이다.
|
|
31
|
+
일회성 외부 의견 수집 도구이므로, 양질의 결과를 위해
|
|
32
|
+
agenda와 context를 최대한 풍부하게 제공해야 한다.
|
|
33
|
+
|
|
34
|
+
[agenda 작성 가이드]
|
|
35
|
+
- 분석/검토/비교를 요청하는 명확한 질문 형태로 작성
|
|
36
|
+
- 검토 범위와 판단 기준을 명시
|
|
37
|
+
- 모호한 표현 대신 구체적인 용어 사용
|
|
38
|
+
|
|
39
|
+
[context 작성 가이드 - 풍부할수록 좋다]
|
|
40
|
+
input 토큰은 저렴하므로 관련 정보를 아끼지 말고 최대한 제공하라.
|
|
41
|
+
패널이 충분한 맥락 없이 일반론만 답하면 가치가 없다.
|
|
42
|
+
구체적 사양, 수치, 제약 조건, 배경 지식, 도메인 맥락,
|
|
43
|
+
이미 결정된 사항, 실무적 제약 등
|
|
44
|
+
판단에 영향을 줄 수 있는 모든 정보를 포함하라.
|
|
45
|
+
"""
|
|
46
|
+
tf = _get_taskforce()
|
|
47
|
+
pool = await tf.discuss_async(agenda, context)
|
|
48
|
+
return pool.model_dump_json(indent=2)
|