Evaluating OO Design0:00
Now, let's talk about general object-oriented design, and whether strictly adhering to it is relevant in this case. Okay, so once again, I'm within our LaraParseAssistant class, and yeah, you'll notice the only thing I've done behind the scenes is added some basic doc blocks, mostly to give the code a bit of room to breathe. Okay, so before we extract a new Client class, I want to point your attention right here. One thing you become good at, the more you code, is what I call the squint test. And that's where you squint your eyes, and you just kind of scan a file, looking for things that look, I don't know, jumbled, or long, or complex. So why don't you do it with me?
things that look, I don't know, jumbled, or long, or complex. So why don't you do it with me? Just squint your eyes, scroll, and you're just looking for things out of the ordinary. And if I do it right now, yeah, this area just feels a little bit, you know, it's not horrible, but it feels like, I don't know, something's missing, right? So here's what I think we can do. Right here, we fetch a run, and then we're basically checking to see if the run is still processing, or if OpenAI is still working. So with that in mind, why don't we assign a name to it? Here's what I'll do.
Refactoring Processing Check1:15
So with that in mind, why don't we assign a name to it? Here's what I'll do. So I'm going to create a new protected method, and we could call it processing, or I even like simple terms, like, are you still working? Okay, that's going to return a boolean. So now I can take all of this code, move it in here, and replace it with a call to this working. So now we're not going to have a do while, we're just going to check what the current status is. So we'd have something like this, all right?
status is. So we'd have something like this, all right? So retrieve the current run, which means I'm going to need an instance of our run, and I'll pass that in. All right. So update the run, and then determine whether or not it is completed. If it's not completed, by definition, it is still working. Okay, so now what we could do is maybe pass this to a while loop. While we're working, sleep for about a second, and then try again. All right.
While we're working, sleep for about a second, and then try again. All right. So once this does return completed, of course, this call will return false, and then we proceed by fetching the messages. All right, so let's confirm this still works. I'll switch to Chrome and refresh. And there we go. We still get the exact same response. Okay, so I think that is a reasonable refactor. Okay, so now let's move on to creating our client.
Introducing a Client Class2:32
Okay, so I think that is a reasonable refactor. Okay, so now let's move on to creating our client. Now, if I scroll the page here, you're going to see countless calls to OpenAI's API. So here's another one. Here's another one. Here's another one. Here's another one. It just keeps happening. And generally, you know, depending upon the size of the project, this may or may not be
It just keeps happening. And generally, you know, depending upon the size of the project, this may or may not be a problem. On that note, I don't know. Something to keep in mind is, from all the years I've been doing this, there's definitely a tendency to want to be an architect, you know, like, oh, we can't just have this within a single file. We need design. We need to adhere to the SOLID principles. So and often what ends up happening is they just take code here, and then they paste it
We need to adhere to the solid principles. So and often what ends up happening is they just take code here, and then they paste it into this new file, and then they pat themselves on the back for creating better design or allowing for better design. And I know maybe sometimes that happens. It does have its benefits. It allows you to isolate things better. It allows you to mock a dependency so that you can test faster. But also sometimes it doesn't really do much. You just moved it from here to there, you know.
But also sometimes it doesn't really do much. You just moved it from here to there, you know. And you've got to remember that the size of the people working on the project makes a big difference. If it's just you, the rules are different. I'm sorry. It's just true. The rules are different than if you're a member of Facebook and you're working with literally hundreds of different people. So yeah.
hundreds of different people. So yeah. All I have to say, just keep this in mind. What I'm about to show you is just a possibility. And sometimes you may decide, oh, that's what I should do because I know what's about to happen or I know certain flexibility is needed. But then other times it just doesn't matter. And that's something I've tried to reiterate over the years. All right. Back to work.
All right. Back to work. So what we're going to do is I'll come back to our AI class, and I'll just put it right here. Let's create a new class called Client. Think of this as the client that interacts with OpenAI. So what I could do is find all of the relevant methods that we have here that are specific to an OpenAI call. So for example, let's start at the very top, Retrieving an Assistant. So let's open up our client and maybe add a method called retrieveAssistant.
So for example, let's start at the very top, Retrieving an Assistant. So let's open up our client and maybe add a method called retrieveAssistant. All right, next, this one creates an assistant. So let's do that one. All right, what else? This one uploads a file. So let's add a method called uploadFile. All right, what else? We have one for creating a thread. So createThread.
We have one for creating a Thread. So CreateThread. And then right down here, we have another one for creating a Message. So let's duplicate that as well. And yeah, this may seem redundant, and sometimes it is. But also sometimes it gives you some extra flexibility. Here fetchAllMessages. So this is effectively a getter. We could say getMessages or just messages if you wish. And then finally, we have an API call to start a run.
We could say getMessages or just messages if you wish. And then finally, we have an API call to startRun. So why don't we just call that one run. And then lastly, we have one that checks a run's status. So and yeah, we might want to tweak these names, but this is the 30 second pass. Okay, so now what I'm going to do is take any logic in this left pane here that is specific to OpenAI, and I'm going to move it into this dedicated Client class. So for example, this right here, where we retrieve an assistant, will now go here. Paste it in, return it, and we'll need the assistantId. Just like so.
Wiring Client Into Assistant6:06
Paste it in, return it, and we'll need the assistant ID. Just like so. All right, so now right here, to start, let's just instantiate it directly within the constructor. And we'll assign it as a protected property. So protected client, client. Okay, so now when we retrieve our assistant, we would just replace all of this with $this->client->retrieveAssistant(). Cool, yeah, we're just taking this code and isolating all of it within a single file. So when we're done, the only place where OpenAI calls are made will be within this dedicated client class.
So when we're done, the only place where OpenAI calls are made will be within this dedicated Client class. All right, let's do another one. Create. Well, we have one right here. So I'm going to grab all of that, move it over, return the result. And yeah, once again, we can't assume anything right here, because of course, this is generic. So why don't I select all of that and just accept the configuration, like so. All right, so let's, once again, say this Client create assistant.
So why don't I select all of that and just accept the configuration, like so. All right, so let's, once again, say Client::createAssistant(). All right, so now, oh, this is a static constructor. So once again, I would have to say Client, and that's okay. We'll do it directly within here. Client, and then createAssistant(). And yeah, I think that would work. Okay, so while we're here, a quick little voice scouting. Voice scouting, let's inline that. And then I'm going to take our defaultConfig and extract it.
Voice scouting, let's inline that. And then I'm going to take our default config and extract it. You don't have to do this, but in this case, it might help when I come back to the code six months from now to quickly see what are our defaults. All right, our default config, we instantiate a client, we create a new assistant that will physically create the client on OpenAI's end. And then we return a new instance of our assistant. All right, cool. What's next?
All right, cool. What's next? Educate. So we're going to have all of this code, and I'm going to move this over to client. All right, this will need our file. Again, eventually, this could be a file or an array. This will upload the file, and then it will associate the file with the assistant. And that's okay.
and then it will associate the file with the assistant. And that's okay. In this case, we're sort of doing more than simply uploading a file. But for our context, they go hand in hand, so it's fine. Now, notice in this case, we do need access to a current assistant. So why don't we accept that, like so. And then I can update it here. All right, clean that up, like so. All right, let's head back. Okay, so now all of this can be replaced with this clientUploadFile.
All right, let's head back. Okay, so now all of this can be replaced with this client upload file. And then we can't forget to pass the assistant. All right, and cleans up this file a little bit too, which is good, especially if these OpenAI calls become a little more complicated. Maybe we want to take the response and wrap it up within one of our own classes, which is likely something we'd want to do. Even if it's just sort of a plain old php object, we now have a dedicated space where we could do that. Okay, next, creating a thread.
we now have a dedicated space where we could do that. Okay, next, creating a thread. Yeah, it's kind of tedious at this point, but this is what we're doing. So I'll paste that in. Here we go. We'll have our array of parameters, like so, and return the response. Let's switch back. thread equals this->client->createThread(). There we go. One more for fetching all messages.
There we go. One more for fetching all messages. So right here, we're going to need the threadId for the messages that we want to use. All right, switch back. This clientMessages. Yeah, and often you'll find you have situations like this, where you have a method, and then the body of the method is just making a call to a different class with the same method. And that's okay. That's what we want in this case. All right, next, we're going to write a new message.
That's what we want in this case. All right, next, we're going to write a new message. Copy that. Here we go. All right, looks like we need the threadId as well as the message. So let's make the message the first parameter or the first argument, and then the second one is the ID of the thread. That's fine. All right, get rid of this. This client createMessage, and then we'll pass that in.
All right, get rid of this. This client creates message, and then we'll pass that in. Also, one thing we might want to do is, if we're going to use this threadId, we're going to need to create a new message. Also, one thing we might want to do is, the right method to check whether or not we've created a thread. And if we haven't, it will do that for you. I'm just skipping that step in this case. So what's going on here? Create message.
So what's going on here? Create message. Oh yeah, give us the threadId too. All right, next, we're going to send it. So yeah, all of this would be replaced with a call to this client->run(). And yeah, once again, I know bare minimum, we'll need the threadId, and then also the assistant. So we're going to do this in reverse, this assistant. Okay, I think we can take all of that and return it. Okay, let's go into the run method, paste that in.
Okay, I think we can take all of that and return it. Okay, let's go into the run method, paste that in. This accepts the thread ID, and then the assistant, and that's an assistant response. So yeah, keep in mind, in real life, we would need to further extract. So notice we are assuming that we're going to be handed an assistant response, but that may not be the case. So this is where you would want to further take these responses from OpenAI and wrap them up within a class that you are in charge of, and that you adhere to a specific interface.
and wrap them up within a class that you are in charge of, and that you adhere to a specific interface that potentially any implementation could follow. But yeah, we're just not doing that because, you know, we only have so much time. All right, let's update this. Like so. Next, while working... Well, let's come back, and here's this code. And really, all of this can now move over.
Well, let's come back, and here's this code. And really, all of this can now move over. All right, this will accept the threadRunResponse, like so. Let's check the runStatus. And then we return the thread... What is it? threadMessageListResponse. I think that's it. And message just needs the threadId. All right, threadId.
And message just needs the thread ID. All right, thread ID. And I think that's it. What's wrong here? We just need to return a Boolean. Okay, so yeah, I want you to notice... Hold on, let's update some of this real quick. Any other squiggly lines? No, we're good. Oh, one more.
No, we're good. Oh, one more. Okay, so notice the only code that exists in this client is code that makes a call to OpenAI. And yeah, once again, I just want to drill this in. We're still returning OpenAI-specific responses. So that OpenAI reality is leaking out of this class and back into our assistant. And ideally, it would be nice if the assistant didn't even know which AI service we are working with.
And ideally, it would be nice if the assistant didn't even know which AI service we are working with. It just needs to know, is there a way to write a message? Is there a way to run all of the messages and receive a response? And whether OpenAI is doing that, or Google's version, or Apple's, in a perfect world, it wouldn't matter. But once you return specific instances from one AI service, that's what I mean by it's like,
But once you return specific instances from one AI service, that's what I mean by it's like, that reality is leaking out here. And even if you adhere to an interface, it leaks out. All right, stuff to keep in mind. So I can get rid of all of this. And with any luck, I think we should be in pretty good shape. We're just going to do a quick bit of cleanup here, reformat. And yeah, I'm just once again, doing the squint test. Yeah, notice right here,
And yeah, I'm just once again, doing the squint test. Yeah, notice right here, OpenAI specifics have still leaked into this class. And that's something that actually on your own, if you want, if you're working alone, that's something that you could solve. Wrap it up into your own response class. Okay, doing the squint test. Doing the squint test. Is there anything odd here?
Dependency Injection Options14:34
Doing the squint test. Is there anything odd here? Yes, right there. We need to pass the parameters. Yeah, I think this looks good. So now for the constructor, we can instantiate the client here. But if you want to allow for the best testability and isolation, one thing you could do, I'll show you a couple of steps, is you inject it into the constructor.
one thing you could do, I'll show you a couple of steps, is you inject it into the constructor. So what I could do is even assign it in the process, like so, and then I could get rid of that if I want. And that should still work. Let's go into the browser. Give this a refresh. We might have made an error. And yeah, I would have expected that. Too few arguments to function layer parse assistant.
And yeah, I would have expected that. Too few arguments to function parse assistant. So now when we instantiate this, we're no longer passing, or we never updated our routes file to pass the client. So yeah, now this is one thing to be aware of. Once you allow for better testability and you inject these things, it kind of makes your API to the outside world a little more complicated.
it kind of makes your API to the outside world a little more complicated. So I'll show you how we can fix this. But first, yeah, I would have to instantiate a Client and then pass it through. So yeah, it's not a big deal. But on the other hand, now in order to work with Laravel Passport, I have to first instantiate a Client. And then often what you see in documentation is like,
I have to first instantiate a client. And then often what you see in documentation is like, first, instantiate the client, but the client depends on, I'm sorry, let's update this. But the client depends on this adapter or this particular service. So instantiate the service and then inject it into the client and then pass the client to your assistant.
and then inject it into the client and then pass the client to your assistant. And it just gets so complicated very, very quickly. But nonetheless, let's see if this works. And then I'll show you some ideas for how to allow for testability and dependency injection while still keeping the API in terms of how you interact with it pretty clean. Refresh.
in terms of how you interact with it pretty clean. Refresh. Okay, good. I was worried I made another mistake, but it looks like we're in good shape. Okay, so yeah, this is fine. But here's another option. What we could do is omit this. And then what I could say is, well, this can be nullable or optional.
And then what I could say is, well, this can be nullable or optional. So here's what I could do. I'm going to backtrack and bring the protected property up here. And then I won't assign it directly. But what I will do is make it optional like this. And then I could say if we have a client, or more specifically, if we don't have a client, then instantiate one here.
or more specifically, if we don't have a client, then instantiate one here. And then I could assign it like so. But then, of course, if we want, we can merge these lines like so. Okay, so now we allow for that clean API and we instantiate the client directly within the class. But for situations where you want to override it or you want to mock it out, well, you just pass in your own implementation.
Extracting an Interface17:28
or you want to mock it out, well, you just pass in your own implementation. Okay, just a little tip there. What else do we want to discuss making an interface? Yeah, if you want to adhere to the dependency inversion principle, what you could do is take all of this here, is we, well, in this case, we just take the method signature and we extract an interface.
we just take the method signature and we extract an interface. Let's see if phpStorm can do this for me. Extract interface. The interface name, here's what we'll do. Let's call it interface, but then I'm going to, let's call it ClientInterface, but then I'm going to change it. So we'll do this now. All right, so check this out.
So we'll do this now. All right, so check this out. We now have an interface called ClientInterface. But again, I want to remind you, and this is your homework, this interface is supposed to be generic, meaning it could be applied to anything. But right now, it's specifically returning an instance of an OpenAI class, which again, breaks the entire boundary.
of an open AI class, which again, breaks the entire boundary. So you would need to wrap that up. And I want you to do that as your homework. And if you need help, we can work with you within the comments section. But yeah, for now, I'm going to leave it like that. And then our client specifically can implement that interface. Now, here's another tip.
can implement that interface. Now, here's another tip. Generally, I don't like to call things like interface. Instead, the interface should be the generic term. And then your implementation should be the specific term. So in this case, what kind of client is it? Well, it's an open AI client. So I could update this like so. And notice, once I've made this more specific, the interface can become more generic.
And notice, once I've made this more specific, the interface can become more generic. So if I come here, this can simply be AI client. How about that? There we go. And that ends up reading just a little bit better. Open AI. Oh, that's not right. Open AI. Did I do that two times?
Open AI. Did I do that two times? Maybe I did. Cool. So now, yeah, notice if I get rid of any of these methods, my editor is going to squawk because we aren't conforming to the contract that this interface presents us. That's at least how I think of it. It's a contract that you have to adhere to.
That's at least how I think of it. It's a contract that you have to adhere to. Just like signing a legal contract, you have to adhere to the terms. And the same is true for any interface that you implement. Okay. So let's bring that back. Come to our assistant. And now we can update this to say, don't depend on openAI specifically.
And now we can update this to say, don't depend on OpenAI specifically because we don't necessarily care. Just depend on some kind of AI client. And then if you don't present one to us, then we can assume, okay, we'll just default to OpenAI if you don't override that. Okay. We'll leave it there.
Okay. We'll leave it there. And now, yeah, let's talk about it. What are some potential pros and cons? And I'm just going to spitball here. Whatever comes to my head first. A con. It's more complicated. It's always the downside, right? We originally just had a single file
It's always the downside, right? We originally just had a single file and it was pretty easy to understand. Now, it's still easy, but we have another file. We have two more files. We extracted a ClientInterface and then we had an implementation of that interface. Yeah. I mean, there's no way around it.
Yeah. I mean, there's no way around it. It's just a little bit more complicated. I have to click to this file and then to that file. I have to keep the interface in sync with the implementation. I have to be a lot more thoughtful. Some people would say though, no, that's a pro.
Some people would say though, no, that's a pro. You have to take time to really think about what your interface should be called and how you adhere to it. What else? A pro. Definitely, we can test our clients in isolation much more easily. Now, I will let you know though,
in isolation much more easily. Now, I will let you know though, because we're using the Laravel implementation of that OpenAI facade, they are thoughtful enough to include testing helpers. And actually, that's one of the benefits to Laravel facades. Out of the box, you get that testability because they provide behavior to swap out the underlying instance.
because they provide behavior to swap out the underlying instance. And many of us are just so used to the idea, no, this class can only be tested in isolation if all of its dependencies are injected. But it turns out with facades, well, that's not necessarily true. If you're working with Laravel, you can swap that out. But nonetheless, there are some downsides to it.
you can swap that out. But nonetheless, there are some downsides to it. You are now more aware of Laravel. You are more dependent upon Laravel. But then again, if you're in a Laravel app, who cares? That's where all of this stuff comes back to. Do I want to care about this or do I not? And it turns out these are the questions you have to ask yourself.
And it turns out these are the questions you have to ask yourself for every project you're working on. Is this something I care about? Was it worth doing all of this? Maybe if you're on a team. Maybe if you want to be able to support the next Apple AI assistant, if they have something like that, then yes. But maybe it's like you're just building
if they have something like that, then yes. But maybe it's like you're just building a side project for yourself, in which case this is horribly overblown and you just don't need it. All right, and that's going to do it. So yeah, in the next episode, we'll move on to something else. If you want to do the homework for this video, yeah, I want you to visit the interface
If you want to do the homework for this video, yeah, I want you to visit the interface and remove any specific references to OpenAI's responses. Instead, you take those responses and you wrap them up within your own generic response, and then you reference those. So see what you come up with. Leave a comment if you need help.
So see what you come up with. Leave a comment if you need help and we'll be happy to oblige. All right, next episode, something new. I'll see you then.
