-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add workspace chat agent participants #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4a24105
24091bd
3a50964
1aba946
532c87a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ | |
| use App\Support\Connections\ViewerIdentityResolver; | ||
| use Illuminate\Http\RedirectResponse; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Cache; | ||
| use Illuminate\Validation\ValidationException; | ||
|
|
||
| class ChatController extends Controller | ||
| { | ||
|
|
@@ -19,6 +21,23 @@ public function store( | |
| ViewerIdentityResolver $viewerIdentityResolver, | ||
| WorkspaceChatManager $chatManager, | ||
| ): RedirectResponse { | ||
| $submittedToken = (string) $request->validated('chat_submission_token'); | ||
| $expectedToken = $request->session()->get('chat.create_token'); | ||
|
|
||
| if (! is_string($expectedToken) || ! hash_equals($expectedToken, $submittedToken)) { | ||
| return to_route('home'); | ||
| } | ||
|
|
||
| $submissionTokenCacheKey = sprintf( | ||
| 'workspace-chat-create:%d:%s', | ||
| (int) $request->user()->getKey(), | ||
| sha1($submittedToken), | ||
| ); | ||
|
|
||
| if (! Cache::add($submissionTokenCacheKey, true, now()->addMinutes(10))) { | ||
| return to_route('home'); | ||
| } | ||
|
|
||
| $activeConnection = $connectionManager->activeConnectionFor( | ||
| $request->user(), | ||
| $request->root(), | ||
|
|
@@ -33,10 +52,21 @@ public function store( | |
| $activeWorkspace = $connectionManager->activeWorkspaceFor($activeConnection, $workspaces); | ||
| $viewerIdentity = $viewerIdentityResolver->resolve($request->user(), $activeConnection); | ||
|
|
||
| $chatManager->createChat($activeWorkspace, $request->user(), $viewerIdentity, [ | ||
| 'name' => $request->validated('chat_name'), | ||
| 'kind' => $request->validated('chat_kind'), | ||
| ]); | ||
| try { | ||
| $chatManager->createChat($activeWorkspace, $request->user(), $viewerIdentity, [ | ||
| 'name' => $request->validated('chat_name'), | ||
| 'kind' => $request->validated('chat_kind'), | ||
| 'workspace_agent_id' => $request->integer('workspace_agent_id') ?: null, | ||
| ]); | ||
| } catch (ValidationException $exception) { | ||
| Cache::forget($submissionTokenCacheKey); | ||
|
|
||
| throw $exception; | ||
| } | ||
|
|
||
| if (hash_equals((string) $request->session()->get('chat.create_token'), $submittedToken)) { | ||
| $request->session()->forget('chat.create_token'); | ||
| } | ||
|
Comment on lines
24
to
+69
|
||
|
|
||
| return to_route('home'); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| <?php | ||
|
|
||
| namespace App\Models; | ||
|
|
||
| use App\Ai\Agents\WorkspaceGuide; | ||
| use Database\Factories\WorkspaceAgentFactory; | ||
| use Illuminate\Database\Eloquent\Attributes\Fillable; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||
| use Illuminate\Database\Eloquent\Relations\HasMany; | ||
|
|
||
| #[Fillable(['workspace_id', 'agent_key', 'name', 'agent_class', 'summary'])] | ||
| class WorkspaceAgent extends Model | ||
| { | ||
| public const KEY_WORKSPACE_GUIDE = 'workspace-guide'; | ||
|
|
||
| public const CLASS_WORKSPACE_GUIDE = WorkspaceGuide::class; | ||
|
|
||
| /** @use HasFactory<WorkspaceAgentFactory> */ | ||
| use HasFactory; | ||
|
|
||
| /** | ||
| * @return BelongsTo<Workspace, $this> | ||
| */ | ||
| public function workspace(): BelongsTo | ||
| { | ||
| return $this->belongsTo(Workspace::class); | ||
| } | ||
|
|
||
| /** | ||
| * @return HasMany<WorkspaceChatParticipant, $this> | ||
| */ | ||
| public function chatParticipants(): HasMany | ||
| { | ||
| return $this->hasMany(WorkspaceChatParticipant::class, 'workspace_agent_id'); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| <?php | ||
|
|
||
| namespace App\Support\Chats; | ||
|
|
||
| use App\Models\Workspace; | ||
| use App\Models\WorkspaceAgent; | ||
| use App\Models\WorkspaceChatParticipant; | ||
| use Illuminate\Database\Eloquent\Collection; | ||
|
|
||
| class WorkspaceAgentManager | ||
| { | ||
| /** | ||
| * @var array<int, array{agent_key: string, name: string, agent_class: string, summary: string}> | ||
| */ | ||
| private const DEFAULT_AGENTS = [ | ||
| [ | ||
| 'agent_key' => WorkspaceAgent::KEY_WORKSPACE_GUIDE, | ||
| 'name' => 'Workspace Guide', | ||
| 'agent_class' => WorkspaceAgent::CLASS_WORKSPACE_GUIDE, | ||
| 'summary' => 'Helps shape durable, graph-native collaboration inside this workspace.', | ||
| ], | ||
| ]; | ||
|
|
||
| /** | ||
| * @return Collection<int, WorkspaceAgent> | ||
| */ | ||
| public function agentsFor(Workspace $workspace): Collection | ||
| { | ||
| return $workspace->agents() | ||
| ->orderBy('name') | ||
| ->get() | ||
| ->values(); | ||
| } | ||
|
|
||
| /** | ||
| * @return Collection<int, WorkspaceAgent> | ||
| */ | ||
| public function ensureDefaults(Workspace $workspace): Collection | ||
| { | ||
| $existingAgents = $workspace->agents() | ||
| ->orderBy('id') | ||
| ->get() | ||
| ->groupBy('agent_key'); | ||
|
|
||
| foreach (self::DEFAULT_AGENTS as $definition) { | ||
| $workspaceAgent = $existingAgents | ||
| ->get($definition['agent_key']) | ||
| ?->first(); | ||
|
|
||
| if ($workspaceAgent instanceof WorkspaceAgent) { | ||
| $workspaceAgent->forceFill([ | ||
| 'name' => $definition['name'], | ||
| 'agent_class' => $definition['agent_class'], | ||
| 'summary' => $definition['summary'], | ||
| ]); | ||
|
|
||
| if ($workspaceAgent->isDirty()) { | ||
| $workspaceAgent->save(); | ||
| } | ||
|
|
||
| $duplicateAgents = $existingAgents | ||
| ->get($definition['agent_key']) | ||
| ?->slice(1) | ||
| ->values() ?? collect(); | ||
|
|
||
| $duplicateAgentIds = $duplicateAgents | ||
| ->map(fn (WorkspaceAgent $duplicateAgent): int => (int) $duplicateAgent->getKey()) | ||
| ->all(); | ||
|
|
||
| if ($duplicateAgentIds !== []) { | ||
| WorkspaceChatParticipant::query() | ||
| ->whereIn('workspace_agent_id', $duplicateAgentIds) | ||
| ->get() | ||
| ->each(function (WorkspaceChatParticipant $participant) use ($workspaceAgent): void { | ||
| $participant->forceFill([ | ||
| 'workspace_agent_id' => $workspaceAgent->getKey(), | ||
| 'display_name' => $workspaceAgent->name, | ||
| ])->save(); | ||
| }); | ||
|
|
||
| $duplicateAgents | ||
| ->each(fn (WorkspaceAgent $duplicateAgent): bool => $duplicateAgent->delete()); | ||
| } | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| $workspace->agents()->create([ | ||
| 'agent_key' => $definition['agent_key'], | ||
| 'name' => $definition['name'], | ||
| 'agent_class' => $definition['agent_class'], | ||
| 'summary' => $definition['summary'], | ||
| ]); | ||
ibourgeois marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return $workspace->agents() | ||
| ->orderBy('name') | ||
| ->get() | ||
| ->values(); | ||
| } | ||
|
|
||
| public function agentForWorkspace(Workspace $workspace, ?int $agentId): ?WorkspaceAgent | ||
| { | ||
| if ($agentId === null) { | ||
| return null; | ||
| } | ||
|
|
||
| return $workspace->agents() | ||
| ->whereKey($agentId) | ||
| ->first(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.